From 4188f589d3567e9706cda537ef8057a37f3261eb Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 2 Dec 2014 14:25:49 +0100 Subject: [PATCH 001/857] Set removeOnCancelPolicy on the threadpool if supported --- .../internal/schedulers/NewThreadWorker.java | 24 +++++-- .../schedulers/CachedThreadSchedulerTest.java | 65 ++++++++++++++++++- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index 75240e47e7..17128709db 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -15,15 +15,14 @@ */ package rx.internal.schedulers; -import rx.Scheduler; -import rx.Subscription; +import java.lang.reflect.Method; +import java.util.concurrent.*; + +import rx.*; import rx.functions.Action0; -import rx.plugins.RxJavaPlugins; -import rx.plugins.RxJavaSchedulersHook; +import rx.plugins.*; import rx.subscriptions.Subscriptions; -import java.util.concurrent.*; - /** * @warn class description missing */ @@ -35,6 +34,19 @@ public class NewThreadWorker extends Scheduler.Worker implements Subscription { /* package */ public NewThreadWorker(ThreadFactory threadFactory) { executor = Executors.newScheduledThreadPool(1, threadFactory); + // Java 7+: cancelled future tasks can be removed from the executor thus avoiding memory leak + for (Method m : executor.getClass().getMethods()) { + if (m.getName().equals("setRemoveOnCancelPolicy") + && m.getParameterCount() == 1 + && m.getParameters()[0].getType() == Boolean.TYPE) { + try { + m.invoke(executor, true); + } catch (Exception ex) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); + } + break; + } + } schedulersHook = RxJavaPlugins.getInstance().getSchedulersHook(); } diff --git a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java b/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java index e385e3828d..4b6b02eff2 100644 --- a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java +++ b/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java @@ -16,12 +16,16 @@ package rx.schedulers; +import java.lang.management.*; +import java.util.concurrent.*; + +import junit.framework.Assert; + import org.junit.Test; + import rx.Observable; import rx.Scheduler; -import rx.functions.Action1; -import rx.functions.Func1; - +import rx.functions.*; import static org.junit.Assert.assertTrue; public class CachedThreadSchedulerTest extends AbstractSchedulerConcurrencyTests { @@ -66,4 +70,59 @@ public final void testUnhandledErrorIsDeliveredToThreadHandler() throws Interrup public final void testHandledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { SchedulerTests.testHandledErrorIsNotDeliveredToThreadHandler(getScheduler()); } + + @Test(timeout = 30000) + public void testCancelledTaskRetention() throws InterruptedException { + try { + ScheduledThreadPoolExecutor.class.getMethod("setRemoveOnCancelPolicy", Boolean.TYPE); + + 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); + + Scheduler.Worker w = Schedulers.io().createWorker(); + for (int i = 0; i < 750000; 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(1000); + + 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) { + Assert.fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d)); + } + } catch (NoSuchMethodException ex) { + // not supported, no reason to test for it + } + } + } \ No newline at end of file From 87ea431979b34751587576d542c261318c1c6de4 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 2 Dec 2014 14:54:51 +0100 Subject: [PATCH 002/857] Fixed wrong Java 8 reflection API call --- src/main/java/rx/internal/schedulers/NewThreadWorker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index 17128709db..5354999055 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -37,8 +37,8 @@ public NewThreadWorker(ThreadFactory threadFactory) { // Java 7+: cancelled future tasks can be removed from the executor thus avoiding memory leak for (Method m : executor.getClass().getMethods()) { if (m.getName().equals("setRemoveOnCancelPolicy") - && m.getParameterCount() == 1 - && m.getParameters()[0].getType() == Boolean.TYPE) { + && m.getParameterTypes().length == 1 + && m.getParameterTypes()[0] == Boolean.TYPE) { try { m.invoke(executor, true); } catch (Exception ex) { From 54d4224297fcd4c1f2c6770b1f0766dc3402c374 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Thu, 4 Dec 2014 19:20:27 +0800 Subject: [PATCH 003/857] Move the codes out of the finally block --- .../rx/internal/operators/OperatorMerge.java | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index f270b08eac..9f3f36390b 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -218,15 +218,17 @@ private void handleScalarSynchronousObservable(ScalarSynchronousObservable t) { T value = t.get(); if (getEmitLock()) { + boolean moreToDrain; try { actual.onNext(value); - return; } finally { - if (releaseEmitLock()) { - drainQueuesIfNeeded(); - } - request(1); + moreToDrain = releaseEmitLock(); } + if (moreToDrain) { + drainQueuesIfNeeded(); + } + request(1); + return; } else { initScalarValueQueueIfNeeded(); try { @@ -241,6 +243,8 @@ private void handleScalarSynchronousObservableWithoutRequestLimits(ScalarSynchro private void handleScalarSynchronousObservableWithRequestLimits(ScalarSynchronousObservable t) { if (getEmitLock()) { boolean emitted = false; + boolean moreToDrain; + boolean isReturn = false; try { long r = mergeProducer.requested; if (r > 0) { @@ -248,15 +252,19 @@ private void handleScalarSynchronousObservableWithRequestLimits(ScalarSynchronou actual.onNext(t.get()); MergeProducer.REQUESTED.decrementAndGet(mergeProducer); // we handle this Observable without ever incrementing the wip or touching other machinery so just return here - return; + isReturn = true; } } finally { - if (releaseEmitLock()) { - drainQueuesIfNeeded(); - } - if (emitted) { - request(1); - } + moreToDrain = releaseEmitLock(); + } + if (moreToDrain) { + drainQueuesIfNeeded(); + } + if (emitted) { + request(1); + } + if (isReturn) { + return; } } @@ -301,20 +309,21 @@ private boolean drainQueuesIfNeeded() { while (true) { if (getEmitLock()) { int emitted = 0; + boolean moreToDrain; try { emitted = drainScalarValueQueue(); drainChildrenQueues(); } finally { - boolean moreToDrain = releaseEmitLock(); - // request outside of lock - if (emitted > 0) { - request(emitted); - } - if (!moreToDrain) { - return true; - } - // otherwise we'll loop and get whatever was added + moreToDrain = releaseEmitLock(); + } + // request outside of lock + if (emitted > 0) { + request(emitted); + } + if (!moreToDrain) { + return true; } + // otherwise we'll loop and get whatever was added } else { return false; } From 1d15fea659d5f84bd666b164f3bf6ebcbb91327d Mon Sep 17 00:00:00 2001 From: Galo Navarro Date: Fri, 21 Nov 2014 13:10:26 -0800 Subject: [PATCH 004/857] Add onBackpressureBuffer with capacity The operator takes an optional capacity for the buffer and a callback that will be invoked if the buffer fills up, along with a MissingBackpressureException in the Observable's onError. --- src/main/java/rx/Observable.java | 43 +++++++++++ .../OperatorOnBackpressureBuffer.java | 76 +++++++++++++++++-- .../OperatorOnBackpressureBufferTest.java | 60 ++++++++++++++- 3 files changed, 169 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 5f40f0156e..98e83775a2 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -15,6 +15,7 @@ import java.util.*; import java.util.concurrent.*; +import rx.annotations.Beta; import rx.annotations.Experimental; import rx.exceptions.*; import rx.functions.*; @@ -5035,6 +5036,48 @@ public final Observable onBackpressureBuffer() { return lift(new OperatorOnBackpressureBuffer()); } + /** + * 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 {@code onError} emitting a + * {@link java.nio.BufferOverflowException} as soon as the buffer's capacity is exceeded, dropping all + * undelivered items, and unsubscribing from the source. + *

+ * + *

+ *
Scheduler:
+ *
{@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. + * @see RxJava wiki: Backpressure + * @Beta + */ + @Beta + public final Observable onBackpressureBuffer(long capacity) { + return lift(new OperatorOnBackpressureBuffer(capacity)); + } + + /** + * 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 {@code onError} emitting a + * {@link java.nio.BufferOverflowException} as soon as the buffer's capacity is exceeded, dropping all + * undelivered items, unsubscribing from the source, and notifying the producer with {@code onOverflow}. + *

+ * + *

+ *
Scheduler:
+ *
{@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. + * @see RxJava wiki: Backpressure + * @Beta + */ + @Beta + public final Observable onBackpressureBuffer(long capacity, Action0 onOverflow) { + return lift(new OperatorOnBackpressureBuffer(capacity, onOverflow)); + } + /** * 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 f288132db9..2ddb582f9e 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -17,21 +17,44 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; +import rx.exceptions.MissingBackpressureException; +import rx.functions.Action0; public class OperatorOnBackpressureBuffer implements Operator { private final NotificationLite on = NotificationLite.instance(); + private final Long capacity; + private final Action0 onOverflow; + + public OperatorOnBackpressureBuffer() { + this.capacity = null; + this.onOverflow = null; + } + + public OperatorOnBackpressureBuffer(long capacity) { + this(capacity, null); + } + + public OperatorOnBackpressureBuffer(long capacity, Action0 onOverflow) { + if (capacity <= 0) { + throw new IllegalArgumentException("Buffer capacity must be > 0"); + } + this.capacity = capacity; + this.onOverflow = onOverflow; + } + @Override public Subscriber call(final Subscriber child) { // TODO get a different queue implementation - // TODO start with size hint final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + final AtomicLong capacity = (this.capacity == null) ? null : new AtomicLong(this.capacity); final AtomicLong wip = new AtomicLong(); final AtomicLong requested = new AtomicLong(); @@ -40,7 +63,7 @@ public Subscriber call(final Subscriber child) { @Override public void request(long n) { if (requested.getAndAdd(n) == 0) { - pollQueue(wip, requested, queue, child); + pollQueue(wip, requested, capacity, queue, child); } } @@ -48,6 +71,9 @@ public void request(long n) { // don't pass through subscriber as we are async and doing queue draining // a parent being unsubscribed should not affect the children Subscriber parent = new Subscriber() { + + private AtomicBoolean saturated = new AtomicBoolean(false); + @Override public void onStart() { request(Long.MAX_VALUE); @@ -55,22 +81,53 @@ public void onStart() { @Override public void onCompleted() { - queue.offer(on.completed()); - pollQueue(wip, requested, queue, child); + if (!saturated.get()) { + queue.offer(on.completed()); + pollQueue(wip, requested, capacity, queue, child); + } } @Override public void onError(Throwable e) { - queue.offer(on.error(e)); - pollQueue(wip, requested, queue, child); + if (!saturated.get()) { + queue.offer(on.error(e)); + pollQueue(wip, requested, capacity, queue, child); + } } @Override public void onNext(T t) { + if (!assertCapacity()) { + return; + } queue.offer(on.next(t)); - pollQueue(wip, requested, queue, child); + pollQueue(wip, requested, capacity, queue, child); } + private boolean assertCapacity() { + if (capacity == null) { + return true; + } + + long currCapacity; + do { + currCapacity = capacity.get(); + if (currCapacity <= 0) { + if (saturated.compareAndSet(false, true)) { + unsubscribe(); + child.onError(new MissingBackpressureException( + "Overflowed buffer of " + + OperatorOnBackpressureBuffer.this.capacity)); + if (onOverflow != null) { + onOverflow.call(); + } + } + return false; + } + // ensure no other thread stole our slot, or retry + } while (!capacity.compareAndSet(currCapacity, currCapacity - 1)); + return true; + } }; // if child unsubscribes it should unsubscribe the parent, but not the other way around @@ -79,7 +136,7 @@ public void onNext(T t) { return parent; } - private void pollQueue(AtomicLong wip, AtomicLong requested, Queue queue, Subscriber child) { + private void pollQueue(AtomicLong wip, AtomicLong requested, AtomicLong capacity, Queue queue, Subscriber child) { // TODO can we do this without putting everything in the queue first so we can fast-path the case when we don't need to queue? if (requested.get() > 0) { // only one draining at a time @@ -96,6 +153,9 @@ private void pollQueue(AtomicLong wip, AtomicLong requested, Queue queue requested.incrementAndGet(); return; } + if (capacity != null) { // it's bounded + capacity.incrementAndGet(); + } on.accept(child, o); } else { // we hit the end ... so increment back to 0 again diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java index 5ca64a0874..34cb73cfc0 100644 --- a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java @@ -15,9 +15,8 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; - import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -25,9 +24,15 @@ import rx.Observable.OnSubscribe; import rx.Observer; import rx.Subscriber; +import rx.Subscription; +import rx.exceptions.MissingBackpressureException; +import rx.functions.Action0; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + public class OperatorOnBackpressureBufferTest { @Test @@ -81,6 +86,56 @@ public void onNext(Long t) { assertEquals(499, ts.getOnNextEvents().get(499).intValue()); } + @Test(expected = IllegalArgumentException.class) + public void testFixBackpressureBufferNegativeCapacity() throws InterruptedException { + Observable.empty().onBackpressureBuffer(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testFixBackpressureBufferZeroCapacity() throws InterruptedException { + Observable.empty().onBackpressureBuffer(-1); + } + + @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(); + } + + }); + + ts.requestMore(100); + Subscription s = infinite.subscribeOn(Schedulers.computation()) + .onBackpressureBuffer(500, new Action0() { + @Override + public void call() { + backpressureCallback.countDown(); + } + }).take(1000).subscribe(ts); + l1.await(); + + ts.requestMore(50); + + assertTrue(backpressureCallback.await(500, TimeUnit.MILLISECONDS)); + assertTrue(ts.getOnErrorEvents().get(0) instanceof MissingBackpressureException); + + int size = ts.getOnNextEvents().size(); + assertTrue(size <= 150); // will get up to 50 more + assertTrue(ts.getOnNextEvents().get(size-1) == size-1); + assertTrue(s.isUnsubscribed()); + } + static final Observable infinite = Observable.create(new OnSubscribe() { @Override @@ -92,4 +147,5 @@ public void call(Subscriber s) { } }); + } From 5620449a672d5cdfdd8999c124be7e448c406229 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 8 Dec 2014 09:54:51 +0100 Subject: [PATCH 005/857] Scheduler.Worker to be finally unsubscribed to avoid interference. --- .../AbstractSchedulerConcurrencyTests.java | 409 +++++++++--------- .../rx/schedulers/AbstractSchedulerTests.java | 300 +++++++------ .../schedulers/ComputationSchedulerTests.java | 80 ++-- .../java/rx/schedulers/TestSchedulerTest.java | 261 ++++++----- .../schedulers/TrampolineSchedulerTest.java | 71 +-- 5 files changed, 598 insertions(+), 523 deletions(-) diff --git a/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java b/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java index 803d999fe8..2eab100310 100644 --- a/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java +++ b/src/test/java/rx/schedulers/AbstractSchedulerConcurrencyTests.java @@ -101,44 +101,47 @@ public void testUnsubscribeRecursiveScheduleFromOutside() throws InterruptedExce final CountDownLatch unsubscribeLatch = new CountDownLatch(1); final AtomicInteger counter = new AtomicInteger(); final Worker inner = getScheduler().createWorker(); - - inner.schedule(new Action0() { - - @Override - public void call() { - inner.schedule(new Action0() { - - int i = 0; - - @Override - public void call() { - System.out.println("Run: " + i++); - if (i == 10) { - latch.countDown(); - try { - // wait for unsubscribe to finish so we are not racing it - unsubscribeLatch.await(); - } catch (InterruptedException e) { - // we expect the countDown if unsubscribe is not working - // or to be interrupted if unsubscribe is successful since - // the unsubscribe will interrupt it as it is calling Future.cancel(true) - // so we will ignore the stacktrace + try { + inner.schedule(new Action0() { + + @Override + public void call() { + inner.schedule(new Action0() { + + int i = 0; + + @Override + public void call() { + System.out.println("Run: " + i++); + if (i == 10) { + latch.countDown(); + try { + // wait for unsubscribe to finish so we are not racing it + unsubscribeLatch.await(); + } catch (InterruptedException e) { + // we expect the countDown if unsubscribe is not working + // or to be interrupted if unsubscribe is successful since + // the unsubscribe will interrupt it as it is calling Future.cancel(true) + // so we will ignore the stacktrace + } } + + counter.incrementAndGet(); + inner.schedule(this); } - - counter.incrementAndGet(); - inner.schedule(this); - } - }); - } - - }); - - latch.await(); - inner.unsubscribe(); - unsubscribeLatch.countDown(); - Thread.sleep(200); // let time pass to see if the scheduler is still doing work - assertEquals(10, counter.get()); + }); + } + + }); + + latch.await(); + inner.unsubscribe(); + unsubscribeLatch.countDown(); + Thread.sleep(200); // let time pass to see if the scheduler is still doing work + assertEquals(10, counter.get()); + } finally { + inner.unsubscribe(); + } } @Test @@ -146,32 +149,36 @@ public void testUnsubscribeRecursiveScheduleFromInside() throws InterruptedExcep final CountDownLatch unsubscribeLatch = new CountDownLatch(1); final AtomicInteger counter = new AtomicInteger(); final Worker inner = getScheduler().createWorker(); - inner.schedule(new Action0() { - - @Override - public void call() { - inner.schedule(new Action0() { - - int i = 0; - - @Override - public void call() { - System.out.println("Run: " + i++); - if (i == 10) { - inner.unsubscribe(); + try { + inner.schedule(new Action0() { + + @Override + public void call() { + inner.schedule(new Action0() { + + int i = 0; + + @Override + public void call() { + System.out.println("Run: " + i++); + if (i == 10) { + inner.unsubscribe(); + } + + counter.incrementAndGet(); + inner.schedule(this); } - - counter.incrementAndGet(); - inner.schedule(this); - } - }); - } - - }); - - unsubscribeLatch.countDown(); - Thread.sleep(200); // let time pass to see if the scheduler is still doing work - assertEquals(10, counter.get()); + }); + } + + }); + + unsubscribeLatch.countDown(); + Thread.sleep(200); // let time pass to see if the scheduler is still doing work + assertEquals(10, counter.get()); + } finally { + inner.unsubscribe(); + } } @Test @@ -180,91 +187,104 @@ public void testUnsubscribeRecursiveScheduleWithDelay() throws InterruptedExcept final CountDownLatch unsubscribeLatch = new CountDownLatch(1); final AtomicInteger counter = new AtomicInteger(); final Worker inner = getScheduler().createWorker(); - inner.schedule(new Action0() { - - @Override - public void call() { - inner.schedule(new Action0() { - - long i = 1L; - - @Override - public void call() { - if (i++ == 10) { - latch.countDown(); - try { - // wait for unsubscribe to finish so we are not racing it - unsubscribeLatch.await(); - } catch (InterruptedException e) { - // we expect the countDown if unsubscribe is not working - // or to be interrupted if unsubscribe is successful since - // the unsubscribe will interrupt it as it is calling Future.cancel(true) - // so we will ignore the stacktrace + + try { + inner.schedule(new Action0() { + + @Override + public void call() { + inner.schedule(new Action0() { + + long i = 1L; + + @Override + public void call() { + if (i++ == 10) { + latch.countDown(); + try { + // wait for unsubscribe to finish so we are not racing it + unsubscribeLatch.await(); + } catch (InterruptedException e) { + // we expect the countDown if unsubscribe is not working + // or to be interrupted if unsubscribe is successful since + // the unsubscribe will interrupt it as it is calling Future.cancel(true) + // so we will ignore the stacktrace + } } + + counter.incrementAndGet(); + inner.schedule(this, 10, TimeUnit.MILLISECONDS); } - - counter.incrementAndGet(); - inner.schedule(this, 10, TimeUnit.MILLISECONDS); - } - }, 10, TimeUnit.MILLISECONDS); - } - }); - - latch.await(); - inner.unsubscribe(); - unsubscribeLatch.countDown(); - Thread.sleep(200); // let time pass to see if the scheduler is still doing work - assertEquals(10, counter.get()); + }, 10, TimeUnit.MILLISECONDS); + } + }); + + latch.await(); + inner.unsubscribe(); + unsubscribeLatch.countDown(); + Thread.sleep(200); // let time pass to see if the scheduler is still doing work + assertEquals(10, counter.get()); + } finally { + inner.unsubscribe(); + } } @Test public void recursionFromOuterActionAndUnsubscribeInside() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final Worker inner = getScheduler().createWorker(); - inner.schedule(new Action0() { - - int i = 0; - - @Override - public void call() { - i++; - if (i % 100000 == 0) { - System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); - } - if (i < 1000000L) { - inner.schedule(this); - } else { - latch.countDown(); + try { + inner.schedule(new Action0() { + + int i = 0; + + @Override + public void call() { + i++; + if (i % 100000 == 0) { + System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); + } + if (i < 1000000L) { + inner.schedule(this); + } else { + latch.countDown(); + } } - } - }); - - latch.await(); + }); + + latch.await(); + } finally { + inner.unsubscribe(); + } } @Test public void testRecursion() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final Worker inner = getScheduler().createWorker(); - inner.schedule(new Action0() { - - private long i = 0; - - @Override - public void call() { - i++; - if (i % 100000 == 0) { - System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); - } - if (i < 1000000L) { - inner.schedule(this); - } else { - latch.countDown(); + try { + inner.schedule(new Action0() { + + private long i = 0; + + @Override + public void call() { + i++; + if (i % 100000 == 0) { + System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); + } + if (i < 1000000L) { + inner.schedule(this); + } else { + latch.countDown(); + } } - } - }); - - latch.await(); + }); + + latch.await(); + } finally { + inner.unsubscribe(); + } } @Test @@ -272,72 +292,75 @@ public void testRecursionAndOuterUnsubscribe() throws InterruptedException { // use latches instead of Thread.sleep final CountDownLatch latch = new CountDownLatch(10); final CountDownLatch completionLatch = new CountDownLatch(1); - - Observable obs = Observable.create(new OnSubscribe() { - @Override - public void call(final Subscriber observer) { - final Worker inner = getScheduler().createWorker(); - inner.schedule(new Action0() { - @Override - public void call() { - observer.onNext(42); - latch.countDown(); - - // this will recursively schedule this task for execution again - inner.schedule(this); - } - }); - - observer.add(Subscriptions.create(new Action0() { - - @Override - public void call() { - inner.unsubscribe(); - observer.onCompleted(); - completionLatch.countDown(); - } - - })); - - } - }); - - final AtomicInteger count = new AtomicInteger(); - final AtomicBoolean completed = new AtomicBoolean(false); - Subscription subscribe = obs.subscribe(new Subscriber() { - @Override - public void onCompleted() { - System.out.println("Completed"); - completed.set(true); - } - - @Override - public void onError(Throwable e) { - System.out.println("Error"); + final Worker inner = getScheduler().createWorker(); + try { + Observable obs = Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber observer) { + inner.schedule(new Action0() { + @Override + public void call() { + observer.onNext(42); + latch.countDown(); + + // this will recursively schedule this task for execution again + inner.schedule(this); + } + }); + + observer.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + inner.unsubscribe(); + observer.onCompleted(); + completionLatch.countDown(); + } + + })); + + } + }); + + final AtomicInteger count = new AtomicInteger(); + final AtomicBoolean completed = new AtomicBoolean(false); + Subscription subscribe = obs.subscribe(new Subscriber() { + @Override + public void onCompleted() { + System.out.println("Completed"); + completed.set(true); + } + + @Override + public void onError(Throwable e) { + System.out.println("Error"); + } + + @Override + public void onNext(Integer args) { + count.incrementAndGet(); + System.out.println(args); + } + }); + + if (!latch.await(5000, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting on onNext latch"); } - - @Override - public void onNext(Integer args) { - count.incrementAndGet(); - System.out.println(args); + + // now unsubscribe and ensure it stops the recursive loop + subscribe.unsubscribe(); + System.out.println("unsubscribe"); + + if (!completionLatch.await(5000, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting on completion latch"); } - }); - - if (!latch.await(5000, TimeUnit.MILLISECONDS)) { - fail("Timed out waiting on onNext latch"); + + // the count can be 10 or higher due to thread scheduling of the unsubscribe vs the scheduler looping to emit the count + assertTrue(count.get() >= 10); + assertTrue(completed.get()); + } finally { + inner.unsubscribe(); } - - // now unsubscribe and ensure it stops the recursive loop - subscribe.unsubscribe(); - System.out.println("unsubscribe"); - - if (!completionLatch.await(5000, TimeUnit.MILLISECONDS)) { - fail("Timed out waiting on completion latch"); - } - - // the count can be 10 or higher due to thread scheduling of the unsubscribe vs the scheduler looping to emit the count - assertTrue(count.get() >= 10); - assertTrue(completed.get()); } @Test diff --git a/src/test/java/rx/schedulers/AbstractSchedulerTests.java b/src/test/java/rx/schedulers/AbstractSchedulerTests.java index afb7d1617c..53f77abcc6 100644 --- a/src/test/java/rx/schedulers/AbstractSchedulerTests.java +++ b/src/test/java/rx/schedulers/AbstractSchedulerTests.java @@ -58,55 +58,59 @@ public abstract class AbstractSchedulerTests { public void testNestedActions() throws InterruptedException { Scheduler scheduler = getScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - final CountDownLatch latch = new CountDownLatch(1); - - final Action0 firstStepStart = mock(Action0.class); - final Action0 firstStepEnd = mock(Action0.class); - - final Action0 secondStepStart = mock(Action0.class); - final Action0 secondStepEnd = mock(Action0.class); - - final Action0 thirdStepStart = mock(Action0.class); - final Action0 thirdStepEnd = mock(Action0.class); - - final Action0 firstAction = new Action0() { - @Override - public void call() { - firstStepStart.call(); - firstStepEnd.call(); - latch.countDown(); - } - }; - final Action0 secondAction = new Action0() { - @Override - public void call() { - secondStepStart.call(); - inner.schedule(firstAction); - secondStepEnd.call(); - - } - }; - final Action0 thirdAction = new Action0() { - @Override - public void call() { - thirdStepStart.call(); - inner.schedule(secondAction); - thirdStepEnd.call(); - } - }; - - InOrder inOrder = inOrder(firstStepStart, firstStepEnd, secondStepStart, secondStepEnd, thirdStepStart, thirdStepEnd); - - inner.schedule(thirdAction); - - latch.await(); - - inOrder.verify(thirdStepStart, times(1)).call(); - inOrder.verify(thirdStepEnd, times(1)).call(); - inOrder.verify(secondStepStart, times(1)).call(); - inOrder.verify(secondStepEnd, times(1)).call(); - inOrder.verify(firstStepStart, times(1)).call(); - inOrder.verify(firstStepEnd, times(1)).call(); + try { + final CountDownLatch latch = new CountDownLatch(1); + + final Action0 firstStepStart = mock(Action0.class); + final Action0 firstStepEnd = mock(Action0.class); + + final Action0 secondStepStart = mock(Action0.class); + final Action0 secondStepEnd = mock(Action0.class); + + final Action0 thirdStepStart = mock(Action0.class); + final Action0 thirdStepEnd = mock(Action0.class); + + final Action0 firstAction = new Action0() { + @Override + public void call() { + firstStepStart.call(); + firstStepEnd.call(); + latch.countDown(); + } + }; + final Action0 secondAction = new Action0() { + @Override + public void call() { + secondStepStart.call(); + inner.schedule(firstAction); + secondStepEnd.call(); + + } + }; + final Action0 thirdAction = new Action0() { + @Override + public void call() { + thirdStepStart.call(); + inner.schedule(secondAction); + thirdStepEnd.call(); + } + }; + + InOrder inOrder = inOrder(firstStepStart, firstStepEnd, secondStepStart, secondStepEnd, thirdStepStart, thirdStepEnd); + + inner.schedule(thirdAction); + + latch.await(); + + inOrder.verify(thirdStepStart, times(1)).call(); + inOrder.verify(thirdStepEnd, times(1)).call(); + inOrder.verify(secondStepStart, times(1)).call(); + inOrder.verify(secondStepEnd, times(1)).call(); + inOrder.verify(firstStepStart, times(1)).call(); + inOrder.verify(firstStepEnd, times(1)).call(); + } finally { + inner.unsubscribe(); + } } @Test @@ -194,31 +198,34 @@ public void testSequenceOfDelayedActions() throws InterruptedException { Scheduler scheduler = getScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - final CountDownLatch latch = new CountDownLatch(1); - final Action0 first = mock(Action0.class); - final Action0 second = mock(Action0.class); - - inner.schedule(new Action0() { - @Override - public void call() { - inner.schedule(first, 30, TimeUnit.MILLISECONDS); - inner.schedule(second, 10, TimeUnit.MILLISECONDS); - inner.schedule(new Action0() { - - @Override - public void call() { - latch.countDown(); - } - }, 40, TimeUnit.MILLISECONDS); - } - }); - - latch.await(); - InOrder inOrder = inOrder(first, second); - - inOrder.verify(second, times(1)).call(); - inOrder.verify(first, times(1)).call(); - + try { + final CountDownLatch latch = new CountDownLatch(1); + final Action0 first = mock(Action0.class); + final Action0 second = mock(Action0.class); + + inner.schedule(new Action0() { + @Override + public void call() { + inner.schedule(first, 30, TimeUnit.MILLISECONDS); + inner.schedule(second, 10, TimeUnit.MILLISECONDS); + inner.schedule(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + }, 40, TimeUnit.MILLISECONDS); + } + }); + + latch.await(); + InOrder inOrder = inOrder(first, second); + + inOrder.verify(second, times(1)).call(); + inOrder.verify(first, times(1)).call(); + } finally { + inner.unsubscribe(); + } } @Test @@ -226,85 +233,100 @@ public void testMixOfDelayedAndNonDelayedActions() throws InterruptedException { Scheduler scheduler = getScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - final CountDownLatch latch = new CountDownLatch(1); - final Action0 first = mock(Action0.class); - final Action0 second = mock(Action0.class); - final Action0 third = mock(Action0.class); - final Action0 fourth = mock(Action0.class); - - inner.schedule(new Action0() { - @Override - public void call() { - inner.schedule(first); - inner.schedule(second, 300, TimeUnit.MILLISECONDS); - inner.schedule(third, 100, TimeUnit.MILLISECONDS); - inner.schedule(fourth); - inner.schedule(new Action0() { - - @Override - public void call() { - latch.countDown(); - } - }, 400, TimeUnit.MILLISECONDS); - } - }); - - latch.await(); - InOrder inOrder = inOrder(first, second, third, fourth); - - inOrder.verify(first, times(1)).call(); - inOrder.verify(fourth, times(1)).call(); - inOrder.verify(third, times(1)).call(); - inOrder.verify(second, times(1)).call(); + try { + final CountDownLatch latch = new CountDownLatch(1); + final Action0 first = mock(Action0.class); + final Action0 second = mock(Action0.class); + final Action0 third = mock(Action0.class); + final Action0 fourth = mock(Action0.class); + + inner.schedule(new Action0() { + @Override + public void call() { + inner.schedule(first); + inner.schedule(second, 300, TimeUnit.MILLISECONDS); + inner.schedule(third, 100, TimeUnit.MILLISECONDS); + inner.schedule(fourth); + inner.schedule(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + }, 400, TimeUnit.MILLISECONDS); + } + }); + + latch.await(); + InOrder inOrder = inOrder(first, second, third, fourth); + + inOrder.verify(first, times(1)).call(); + inOrder.verify(fourth, times(1)).call(); + inOrder.verify(third, times(1)).call(); + inOrder.verify(second, times(1)).call(); + } finally { + inner.unsubscribe(); + } } @Test public final void testRecursiveExecution() throws InterruptedException { final Scheduler scheduler = getScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - final AtomicInteger i = new AtomicInteger(); - final CountDownLatch latch = new CountDownLatch(1); - inner.schedule(new Action0() { - - @Override - public void call() { - if (i.incrementAndGet() < 100) { - inner.schedule(this); - } else { - latch.countDown(); + + try { + + final AtomicInteger i = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + inner.schedule(new Action0() { + + @Override + public void call() { + if (i.incrementAndGet() < 100) { + inner.schedule(this); + } else { + latch.countDown(); + } } - } - }); - - latch.await(); - assertEquals(100, i.get()); + }); + + latch.await(); + assertEquals(100, i.get()); + } finally { + inner.unsubscribe(); + } } @Test public final void testRecursiveExecutionWithDelayTime() throws InterruptedException { Scheduler scheduler = getScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - final AtomicInteger i = new AtomicInteger(); - final CountDownLatch latch = new CountDownLatch(1); - - inner.schedule(new Action0() { - - int state = 0; - - @Override - public void call() { - i.set(state); - if (state++ < 100) { - inner.schedule(this, 1, TimeUnit.MILLISECONDS); - } else { - latch.countDown(); + + try { + final AtomicInteger i = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + inner.schedule(new Action0() { + + int state = 0; + + @Override + public void call() { + i.set(state); + if (state++ < 100) { + inner.schedule(this, 1, TimeUnit.MILLISECONDS); + } else { + latch.countDown(); + } } - } - - }); - - latch.await(); - assertEquals(100, i.get()); + + }); + + latch.await(); + assertEquals(100, i.get()); + } finally { + inner.unsubscribe(); + } } @Test diff --git a/src/test/java/rx/schedulers/ComputationSchedulerTests.java b/src/test/java/rx/schedulers/ComputationSchedulerTests.java index 32f15b3564..881224cfac 100644 --- a/src/test/java/rx/schedulers/ComputationSchedulerTests.java +++ b/src/test/java/rx/schedulers/ComputationSchedulerTests.java @@ -46,47 +46,51 @@ public void testThreadSafetyWhenSchedulerIsHoppingBetweenThreads() { final HashMap map = new HashMap(); final Scheduler.Worker inner = Schedulers.computation().createWorker(); - - inner.schedule(new Action0() { - - private HashMap statefulMap = map; - int nonThreadSafeCounter = 0; - - @Override - public void call() { - Integer i = statefulMap.get("a"); - if (i == null) { - i = 1; - statefulMap.put("a", i); - statefulMap.put("b", i); - } else { - i++; - statefulMap.put("a", i); - statefulMap.put("b", i); - } - nonThreadSafeCounter++; - statefulMap.put("nonThreadSafeCounter", nonThreadSafeCounter); - if (i < NUM) { - inner.schedule(this); - } else { - latch.countDown(); + + try { + inner.schedule(new Action0() { + + private HashMap statefulMap = map; + int nonThreadSafeCounter = 0; + + @Override + public void call() { + Integer i = statefulMap.get("a"); + if (i == null) { + i = 1; + statefulMap.put("a", i); + statefulMap.put("b", i); + } else { + i++; + statefulMap.put("a", i); + statefulMap.put("b", i); + } + nonThreadSafeCounter++; + statefulMap.put("nonThreadSafeCounter", nonThreadSafeCounter); + if (i < NUM) { + inner.schedule(this); + } else { + latch.countDown(); + } } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); } - }); - - try { - latch.await(); - } catch (InterruptedException e) { - e.printStackTrace(); + + System.out.println("Count A: " + map.get("a")); + System.out.println("Count B: " + map.get("b")); + System.out.println("nonThreadSafeCounter: " + map.get("nonThreadSafeCounter")); + + assertEquals(NUM, map.get("a").intValue()); + assertEquals(NUM, map.get("b").intValue()); + assertEquals(NUM, map.get("nonThreadSafeCounter").intValue()); + } finally { + inner.unsubscribe(); } - - System.out.println("Count A: " + map.get("a")); - System.out.println("Count B: " + map.get("b")); - System.out.println("nonThreadSafeCounter: " + map.get("nonThreadSafeCounter")); - - assertEquals(NUM, map.get("a").intValue()); - assertEquals(NUM, map.get("b").intValue()); - assertEquals(NUM, map.get("nonThreadSafeCounter").intValue()); } @Test diff --git a/src/test/java/rx/schedulers/TestSchedulerTest.java b/src/test/java/rx/schedulers/TestSchedulerTest.java index 8ab361f54d..f8a49eb0be 100644 --- a/src/test/java/rx/schedulers/TestSchedulerTest.java +++ b/src/test/java/rx/schedulers/TestSchedulerTest.java @@ -48,37 +48,41 @@ public final void testPeriodicScheduling() { final TestScheduler scheduler = new TestScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - inner.schedulePeriodically(new Action0() { - @Override - public void call() { - System.out.println(scheduler.now()); - calledOp.call(scheduler.now()); - } - }, 1, 2, TimeUnit.SECONDS); - - verify(calledOp, never()).call(anyLong()); - - InOrder inOrder = Mockito.inOrder(calledOp); - - scheduler.advanceTimeBy(999L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, never()).call(anyLong()); - - scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, times(1)).call(1000L); - - scheduler.advanceTimeBy(1999L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, never()).call(3000L); - - scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, times(1)).call(3000L); - - scheduler.advanceTimeBy(5L, TimeUnit.SECONDS); - inOrder.verify(calledOp, times(1)).call(5000L); - inOrder.verify(calledOp, times(1)).call(7000L); - - inner.unsubscribe(); - scheduler.advanceTimeBy(11L, TimeUnit.SECONDS); - inOrder.verify(calledOp, never()).call(anyLong()); + try { + inner.schedulePeriodically(new Action0() { + @Override + public void call() { + System.out.println(scheduler.now()); + calledOp.call(scheduler.now()); + } + }, 1, 2, TimeUnit.SECONDS); + + verify(calledOp, never()).call(anyLong()); + + InOrder inOrder = Mockito.inOrder(calledOp); + + scheduler.advanceTimeBy(999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).call(anyLong()); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).call(1000L); + + scheduler.advanceTimeBy(1999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).call(3000L); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).call(3000L); + + scheduler.advanceTimeBy(5L, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(1)).call(5000L); + inOrder.verify(calledOp, times(1)).call(7000L); + + inner.unsubscribe(); + scheduler.advanceTimeBy(11L, TimeUnit.SECONDS); + inOrder.verify(calledOp, never()).call(anyLong()); + } finally { + inner.unsubscribe(); + } } @SuppressWarnings("unchecked") @@ -90,37 +94,41 @@ public final void testPeriodicSchedulingUnsubscription() { final TestScheduler scheduler = new TestScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - final Subscription subscription = inner.schedulePeriodically(new Action0() { - @Override - public void call() { - System.out.println(scheduler.now()); - calledOp.call(scheduler.now()); - } - }, 1, 2, TimeUnit.SECONDS); - - verify(calledOp, never()).call(anyLong()); - - InOrder inOrder = Mockito.inOrder(calledOp); - - scheduler.advanceTimeBy(999L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, never()).call(anyLong()); - - scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, times(1)).call(1000L); - - scheduler.advanceTimeBy(1999L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, never()).call(3000L); - - scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); - inOrder.verify(calledOp, times(1)).call(3000L); - - scheduler.advanceTimeBy(5L, TimeUnit.SECONDS); - inOrder.verify(calledOp, times(1)).call(5000L); - inOrder.verify(calledOp, times(1)).call(7000L); - - subscription.unsubscribe(); - scheduler.advanceTimeBy(11L, TimeUnit.SECONDS); - inOrder.verify(calledOp, never()).call(anyLong()); + try { + final Subscription subscription = inner.schedulePeriodically(new Action0() { + @Override + public void call() { + System.out.println(scheduler.now()); + calledOp.call(scheduler.now()); + } + }, 1, 2, TimeUnit.SECONDS); + + verify(calledOp, never()).call(anyLong()); + + InOrder inOrder = Mockito.inOrder(calledOp); + + scheduler.advanceTimeBy(999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).call(anyLong()); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).call(1000L); + + scheduler.advanceTimeBy(1999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).call(3000L); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).call(3000L); + + scheduler.advanceTimeBy(5L, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(1)).call(5000L); + inOrder.verify(calledOp, times(1)).call(7000L); + + subscription.unsubscribe(); + scheduler.advanceTimeBy(11L, TimeUnit.SECONDS); + inOrder.verify(calledOp, never()).call(anyLong()); + } finally { + inner.unsubscribe(); + } } @Test @@ -129,76 +137,89 @@ public final void testImmediateUnsubscribes() { final Scheduler.Worker inner = s.createWorker(); final AtomicInteger counter = new AtomicInteger(0); - inner.schedule(new Action0() { - - @Override - public void call() { - counter.incrementAndGet(); - System.out.println("counter: " + counter.get()); - inner.schedule(this); - } - - }); - inner.unsubscribe(); - assertEquals(0, counter.get()); + try { + inner.schedule(new Action0() { + + @Override + public void call() { + counter.incrementAndGet(); + System.out.println("counter: " + counter.get()); + inner.schedule(this); + } + + }); + inner.unsubscribe(); + assertEquals(0, counter.get()); + } finally { + inner.unsubscribe(); + } } @Test public final void testImmediateUnsubscribes2() { TestScheduler s = new TestScheduler(); final Scheduler.Worker inner = s.createWorker(); - final AtomicInteger counter = new AtomicInteger(0); - - final Subscription subscription = inner.schedule(new Action0() { - - @Override - public void call() { - counter.incrementAndGet(); - System.out.println("counter: " + counter.get()); - inner.schedule(this); - } - - }); - subscription.unsubscribe(); - assertEquals(0, counter.get()); + try { + final AtomicInteger counter = new AtomicInteger(0); + + final Subscription subscription = inner.schedule(new Action0() { + + @Override + public void call() { + counter.incrementAndGet(); + System.out.println("counter: " + counter.get()); + inner.schedule(this); + } + + }); + subscription.unsubscribe(); + assertEquals(0, counter.get()); + } finally { + inner.unsubscribe(); + } } @Test public final void testNestedSchedule() { final TestScheduler scheduler = new TestScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - final Action0 calledOp = mock(Action0.class); - - Observable poller; - poller = Observable.create(new OnSubscribe() { - @Override - public void call(final Subscriber aSubscriber) { - inner.schedule(new Action0() { - @Override - public void call() { - if (!aSubscriber.isUnsubscribed()) { - calledOp.call(); - inner.schedule(this, 5, TimeUnit.SECONDS); + + try { + final Action0 calledOp = mock(Action0.class); + + Observable poller; + poller = Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber aSubscriber) { + inner.schedule(new Action0() { + @Override + public void call() { + if (!aSubscriber.isUnsubscribed()) { + calledOp.call(); + inner.schedule(this, 5, TimeUnit.SECONDS); + } } - } - }); - } - }); - - InOrder inOrder = Mockito.inOrder(calledOp); - - Subscription sub; - sub = poller.subscribe(); - - scheduler.advanceTimeTo(6, TimeUnit.SECONDS); - inOrder.verify(calledOp, times(2)).call(); - - sub.unsubscribe(); - scheduler.advanceTimeTo(11, TimeUnit.SECONDS); - inOrder.verify(calledOp, never()).call(); - - sub = poller.subscribe(); - scheduler.advanceTimeTo(12, TimeUnit.SECONDS); - inOrder.verify(calledOp, times(1)).call(); + }); + } + }); + + InOrder inOrder = Mockito.inOrder(calledOp); + + Subscription sub; + sub = poller.subscribe(); + + scheduler.advanceTimeTo(6, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(2)).call(); + + sub.unsubscribe(); + scheduler.advanceTimeTo(11, TimeUnit.SECONDS); + inOrder.verify(calledOp, never()).call(); + + sub = poller.subscribe(); + scheduler.advanceTimeTo(12, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(1)).call(); + } finally { + inner.unsubscribe(); + } } } diff --git a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java index 253d3a5382..c628da245f 100644 --- a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java +++ b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java @@ -15,20 +15,17 @@ */ package rx.schedulers; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.*; import org.junit.Test; -import rx.Observable; -import rx.Scheduler; +import rx.*; import rx.Scheduler.Worker; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.Observable; +import rx.functions.*; +import rx.subscriptions.CompositeSubscription; public class TrampolineSchedulerTest extends AbstractSchedulerTests { @@ -65,33 +62,40 @@ public void call(String t) { @Test public void testNestedTrampolineWithUnsubscribe() { final ArrayList workDone = new ArrayList(); + final CompositeSubscription workers = new CompositeSubscription(); Worker worker = Schedulers.trampoline().createWorker(); - worker.schedule(new Action0() { - - @Override - public void call() { - doWorkOnNewTrampoline("A", workDone); - } - - }); - - final Worker worker2 = Schedulers.trampoline().createWorker(); - worker2.schedule(new Action0() { - - @Override - public void call() { - doWorkOnNewTrampoline("B", workDone); - // we unsubscribe worker2 ... it should not affect work scheduled on a separate Trampline.Worker - worker2.unsubscribe(); - } - - }); - - assertEquals(6, workDone.size()); - assertEquals(Arrays.asList("A.1", "A.B.1", "A.B.2", "B.1", "B.B.1", "B.B.2"), workDone); + try { + workers.add(worker); + worker.schedule(new Action0() { + + @Override + public void call() { + workers.add(doWorkOnNewTrampoline("A", workDone)); + } + + }); + + final Worker worker2 = Schedulers.trampoline().createWorker(); + workers.add(worker2); + worker2.schedule(new Action0() { + + @Override + public void call() { + workers.add(doWorkOnNewTrampoline("B", workDone)); + // we unsubscribe worker2 ... it should not affect work scheduled on a separate Trampline.Worker + worker2.unsubscribe(); + } + + }); + + assertEquals(6, workDone.size()); + assertEquals(Arrays.asList("A.1", "A.B.1", "A.B.2", "B.1", "B.B.1", "B.B.2"), workDone); + } finally { + workers.unsubscribe(); + } } - private static void doWorkOnNewTrampoline(final String key, final ArrayList workDone) { + private static Worker doWorkOnNewTrampoline(final String key, final ArrayList workDone) { Worker worker = Schedulers.trampoline().createWorker(); worker.schedule(new Action0() { @@ -106,6 +110,7 @@ public void call() { } }); + return worker; } private static Action0 createPrintAction(final String message, final ArrayList workDone) { From ea2610963d81e7ba9357683884c01f83470cbd67 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 8 Dec 2014 10:12:11 +0100 Subject: [PATCH 006/857] Any should not unsubscribe downstream. --- .../rx/internal/operators/OperatorAny.java | 4 +++- .../internal/operators/OperatorAnyTest.java | 23 ++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorAny.java b/src/main/java/rx/internal/operators/OperatorAny.java index 8d12672550..47e9e017e4 100644 --- a/src/main/java/rx/internal/operators/OperatorAny.java +++ b/src/main/java/rx/internal/operators/OperatorAny.java @@ -36,7 +36,7 @@ public OperatorAny(Func1 predicate, boolean returnOnEmpty) { @Override public Subscriber call(final Subscriber child) { - return new Subscriber(child) { + Subscriber s = new Subscriber() { boolean hasElements; boolean done; @@ -74,5 +74,7 @@ public void onCompleted() { } }; + child.add(s); + return s; } } diff --git a/src/test/java/rx/internal/operators/OperatorAnyTest.java b/src/test/java/rx/internal/operators/OperatorAnyTest.java index 1d3d371fef..6a9e56ce40 100644 --- a/src/test/java/rx/internal/operators/OperatorAnyTest.java +++ b/src/test/java/rx/internal/operators/OperatorAnyTest.java @@ -15,20 +15,16 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertTrue; -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 static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Arrays; +import java.util.concurrent.TimeUnit; import org.junit.Test; -import rx.Observable; -import rx.Observer; +import rx.*; import rx.functions.Func1; -import rx.functions.Functions; import rx.internal.util.UtilityFunctions; public class OperatorAnyTest { @@ -213,4 +209,15 @@ public Boolean call(Integer i) { }); assertTrue(anyEven.toBlocking().first()); } + @Test(timeout = 5000) + public void testIssue1935NoUnsubscribeDownstream() { + Observable source = Observable.just(1).isEmpty() + .flatMap(new Func1>() { + @Override + public Observable call(Boolean t1) { + return Observable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + assertEquals((Object)2, source.toBlocking().first()); + } } From a40a65941d8ae230adcdb4fba3da298abc3bdb81 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 8 Dec 2014 13:05:48 +0100 Subject: [PATCH 007/857] Fixed OperatorAll unsubscribing downstream. --- .../rx/internal/operators/OperatorAll.java | 4 ++- .../internal/operators/OperatorAllTest.java | 31 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorAll.java b/src/main/java/rx/internal/operators/OperatorAll.java index 77145c71cc..24619e3b20 100644 --- a/src/main/java/rx/internal/operators/OperatorAll.java +++ b/src/main/java/rx/internal/operators/OperatorAll.java @@ -34,7 +34,7 @@ public OperatorAll(Func1 predicate) { @Override public Subscriber call(final Subscriber child) { - return new Subscriber(child) { + Subscriber s = new Subscriber() { boolean done; @Override @@ -65,5 +65,7 @@ public void onCompleted() { } } }; + child.add(s); + return s; } } diff --git a/src/test/java/rx/internal/operators/OperatorAllTest.java b/src/test/java/rx/internal/operators/OperatorAllTest.java index e5f4f32a7d..92a2dfa056 100644 --- a/src/test/java/rx/internal/operators/OperatorAllTest.java +++ b/src/test/java/rx/internal/operators/OperatorAllTest.java @@ -15,19 +15,17 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertFalse; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; import org.junit.Test; -import rx.Observable; -import rx.Observer; +import rx.*; import rx.functions.Func1; -import java.util.Arrays; - public class OperatorAllTest { @Test @@ -113,4 +111,21 @@ public Boolean call(Integer i) { }); assertFalse(allOdd.toBlocking().first()); } + @Test(timeout = 5000) + public void testIssue1935NoUnsubscribeDownstream() { + Observable source = Observable.just(1) + .all(new Func1() { + @Override + public Boolean call(Object t1) { + return false; + } + }) + .flatMap(new Func1>() { + @Override + public Observable call(Boolean t1) { + return Observable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + assertEquals((Object)2, source.toBlocking().first()); + } } From 50563ce4617048bfa96d3ea35111c1eab7802fa8 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 9 Dec 2014 08:43:55 +0100 Subject: [PATCH 008/857] AbstractOnSubscribe to help build Observables one onNext at a time. --- .../rx/observables/AbstractOnSubscribe.java | 572 ++++++++++++++++++ .../observables/AbstractOnSubscribeTest.java | 506 ++++++++++++++++ 2 files changed, 1078 insertions(+) create mode 100644 src/main/java/rx/observables/AbstractOnSubscribe.java create mode 100644 src/test/java/rx/observables/AbstractOnSubscribeTest.java diff --git a/src/main/java/rx/observables/AbstractOnSubscribe.java b/src/main/java/rx/observables/AbstractOnSubscribe.java new file mode 100644 index 0000000000..7cbb46b8a1 --- /dev/null +++ b/src/main/java/rx/observables/AbstractOnSubscribe.java @@ -0,0 +1,572 @@ +/** + * 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.observables; + +import java.util.Arrays; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.Observable.OnSubscribe; +import rx.annotations.Experimental; +import rx.exceptions.CompositeException; +import rx.functions.*; + +/** + * Abstract base class for the OnSubscribe interface that helps building + * observable sources one onNext at a time and automatically supports + * unsubscription and backpressure. + *

+ *

Usage rules

+ * Implementors of the {@code next()} method + *
    + *
  • should either + *
      + *
    • create the next value and signal it via {@code state.onNext()},
    • + *
    • signal a terminal condition via {@code state.onError()} or {@code state.onCompleted()} or
    • + *
    • signal a stop condition via {@code state.stop()} indicating no further values will be sent.
    • + *
    + *
  • + *
  • may + *
      + *
    • call {@code state.onNext()} and either {@code state.onError()} or {@code state.onCompleted()} together and + *
    • block or sleep. + *
    + *
  • + *
  • should not + *
      + *
    • do nothing or do async work and not produce any event or request stopping. If neither of + * the methods are called, an {@code IllegalStateException} is forwarded to the {@code Subscriber} and + * the Observable is terminated;
    • + *
    • call the {@code state.onXYZ} methods more than once (yields {@code IllegalStateException}).
    • + *
    + *
  • + *
+ * + * The {@code SubscriptionState} object features counters that may help implement a state machine: + *
    + *
  • A call counter, accessible via {@code state.calls()} that tells how many times + * the {@code next()} was run (zero based). + *
  • A phase counter, accessible via {@code state.phase()} that helps track the current emission + * phase and may be used in a {@code switch ()} statement to implement the state machine. + * (It was named phase to avoid confusion with the per-subscriber state.)
  • + *
  • The current phase can be arbitrarily changed via {@code state.advancePhase()}, + * {@code state.advancePhaseBy(int)} and {@code state.phase(int)}.
  • + * + *
+ *

+ * The implementors of the {@code AbstractOnSubscribe} may override the {@code onSubscribe} to perform + * special actions (such as registering {@code Subscription}s with {@code Subscriber.add()}) and return additional state for each subscriber subscribing. This custom state is + * accessible through the {@code state.state()} method. If the custom state requires some form of cleanup, + * the {@code onTerminated} method can be overridden. + *

+ * For convenience, lambda-accepting static factory methods, named {@code create()}, are available. Another + * convenience is the {@code toObservable} which turns an {@code AbstractOnSubscribe} instance into an {@code Observable} fluently. + * + *

Examples

+ * Note: the examples use the lambda-helper factories to avoid boilerplane. + * + *

Implement: just

+ *

+ * AbstractOnSubscribe.create(s -> {
+ *   s.onNext(1);
+ *   s.onCompleted();
+ * }).toObservable().subscribe(System.out::println);
+ * 
+ + *

Implement: from Iterable

+ *

+ * Iterable iterable = ...;
+ * AbstractOnSubscribe.create(s -> {
+ *   Iterator it = s.state();
+ *   if (it.hasNext()) {
+ *     s.onNext(it.next());
+ *   }
+ *   if (!it.hasNext()) {
+ *     s.onCompleted();
+ *   }
+ * }, u -> iterable.iterator()).subscribe(System.out::println);
+ * 
+ + *

Implement source that fails a number of times before succeeding

+ *

+ * AtomicInteger fails = new AtomicInteger();
+ * int numFails = 50;
+ * AbstractOnSubscribe.create(s -> {
+ *   long c = s.calls();
+ *   switch (s.phase()) {
+ *   case 0:
+ *     s.onNext("Beginning");
+ *     s.onError(new RuntimeException("Oh, failure.");
+ *     if (c == numFails.getAndIncrement()) {
+ *       s.advancePhase();
+ *     }
+ *     break;
+ *   case 1:
+ *     s.onNext("Beginning");
+ *     s.advancePhase();
+ *   case 2:
+ *     s.onNext("Finally working");
+ *     s.onCompleted();
+ *     s.advancePhase();
+ *   default:
+ *     throw new IllegalStateException("How did we get here?");
+ *   }
+ * }).subscribe(System.out::println);
+ * 
+ + *

Implement: never

+ *

+ * AbstractOnSubscribe.create(s -> {
+ *   s.stop();
+ * }).toObservable()
+ * .timeout(1, TimeUnit.SECONDS)
+ * .subscribe(System.out::println, Throwable::printStacktrace, () -> System.out.println("Done"));
+ * 
+ + * @param the value type + * @param the per-subscriber user-defined state type + */ +@Experimental +public abstract class AbstractOnSubscribe implements OnSubscribe { + /** + * Called when a Subscriber subscribes and let's the implementor + * create a per-subscriber custom state. + *

+ * Override this method to have custom state per-subscriber. + * The default implementation returns {@code null}. + * @param subscriber the subscriber who is subscribing + * @return the custom state + */ + protected S onSubscribe(Subscriber subscriber) { + return null; + } + /** + * Called after the terminal emission or when the downstream unsubscribes. + *

+ * This is called only once and it is made sure no onNext call runs concurrently with it. + * The default implementation does nothing. + * @param state the user-provided state + */ + protected void onTerminated(S state) { + + } + /** + * Override this method and create an emission state-machine. + * @param state the per-subscriber subscription state. + */ + protected abstract void next(SubscriptionState state); + + @Override + public final void call(final Subscriber subscriber) { + final S custom = onSubscribe(subscriber); + final SubscriptionState state = new SubscriptionState(this, subscriber, custom); + subscriber.add(new SubscriptionCompleter(state)); + subscriber.setProducer(new SubscriptionProducer(state)); + } + + /** + * Convenience method to create an observable from the implemented instance + * @return the created observable + */ + public final Observable toObservable() { + return Observable.create(this); + } + + /** Function that returns null. */ + private static final Func1 NULL_FUNC1 = new Func1() { + @Override + public Object call(Object t1) { + return null; + } + }; + + /** + * Creates an AbstractOnSubscribe instance which calls the provided {@code next} action. + *

+ * This is a convenience method to help create AbstractOnSubscribe instances with the + * help of lambdas. + * @param the value type + * @param the per-subscriber user-defined state type + * @param next the next action to call + * @return an AbstractOnSubscribe instance + */ + public static AbstractOnSubscribe create(Action1> next) { + @SuppressWarnings("unchecked") + Func1, ? extends S> nullFunc = + (Func1, ? extends S>)NULL_FUNC1; + return create(next, nullFunc, Actions.empty()); + } + /** + * Creates an AbstractOnSubscribe instance which creates a custom state with the + * {@code onSubscribe} function and calls the provided {@code next} action. + *

+ * This is a convenience method to help create AbstractOnSubscribe instances with the + * help of lambdas. + * @param the value type + * @param the per-subscriber user-defined state type + * @param next the next action to call + * @param onSubscribe the function that returns a per-subscriber state to be used by next + * @return an AbstractOnSubscribe instance + */ + public static AbstractOnSubscribe create(Action1> next, + Func1, ? extends S> onSubscribe) { + return create(next, onSubscribe, Actions.empty()); + } + /** + * Creates an AbstractOnSubscribe instance which creates a custom state with the + * {@code onSubscribe} function, calls the provided {@code next} action and + * calls the {@code onTerminated} action to release the state when its no longer needed. + *

+ * This is a convenience method to help create AbstractOnSubscribe instances with the + * help of lambdas. + * @param the value type + * @param the per-subscriber user-defined state type + * @param next the next action to call + * @param onSubscribe the function that returns a per-subscriber state to be used by next + * @param onTerminated the action to call to release the state created by the onSubscribe function + * @return an AbstractOnSubscribe instance + */ + public static AbstractOnSubscribe create(Action1> next, + Func1, ? extends S> onSubscribe, Action1 onTerminated) { + return new LambdaOnSubscribe(next, onSubscribe, onTerminated); + } + /** + * An implementation that forwards the 3 main methods to functional callbacks. + * @param the value type + * @param the per-subscriber user-defined state type + */ + private static final class LambdaOnSubscribe extends AbstractOnSubscribe { + final Action1> next; + final Func1, ? extends S> onSubscribe; + final Action1 onTerminated; + private LambdaOnSubscribe(Action1> next, + Func1, ? extends S> onSubscribe, Action1 onTerminated) { + this.next = next; + this.onSubscribe = onSubscribe; + this.onTerminated = onTerminated; + } + @Override + protected S onSubscribe(Subscriber subscriber) { + return onSubscribe.call(subscriber); + } + @Override + protected void onTerminated(S state) { + onTerminated.call(state); + } + @Override + protected void next(SubscriptionState state) { + next.call(state); + } + } + /** + * Manages unsubscription of the state. + * @param the value type + * @param the per-subscriber user-defined state type + */ + private static final class SubscriptionCompleter extends AtomicBoolean implements Subscription { + /** */ + private static final long serialVersionUID = 7993888274897325004L; + private final SubscriptionState state; + private SubscriptionCompleter(SubscriptionState state) { + this.state = state; + } + @Override + public boolean isUnsubscribed() { + return get(); + } + @Override + public void unsubscribe() { + if (compareAndSet(false, true)) { + state.free(); + } + } + + } + /** + * Contains the producer loop that reacts to downstream requests of work. + * @param the value type + * @param the per-subscriber user-defined state type + */ + private static final class SubscriptionProducer implements Producer { + final SubscriptionState state; + private SubscriptionProducer(SubscriptionState state) { + this.state = state; + } + @Override + public void request(long n) { + if (n == Long.MAX_VALUE) { + for (; !state.subscriber.isUnsubscribed(); ) { + if (!doNext()) { + break; + } + } + } else + if (n > 0 && state.requestCount.getAndAdd(n) == 0) { + if (!state.subscriber.isUnsubscribed()) { + do { + if (!doNext()) { + break; + } + } while (state.requestCount.decrementAndGet() > 0 && !state.subscriber.isUnsubscribed()); + } + } + } + /** + * Executes the user-overridden next() method and performs state bookkeeping and + * verification. + * @return true if the outer loop may continue + */ + protected boolean doNext() { + if (state.use()) { + try { + int p = state.phase(); + state.parent.next(state); + if (!state.verify()) { + throw new IllegalStateException("No event produced or stop called @ Phase: " + p + " -> " + state.phase() + ", Calls: " + state.calls()); + } + if (state.accept() || state.stopRequested()) { + state.terminate(); + return false; + } + state.calls++; + } catch (Throwable t) { + state.terminate(); + state.subscriber.onError(t); + return false; + } finally { + state.free(); + } + return true; + } + return false; + } + } + /** + * Represents a per-subscription state for the AbstractOnSubscribe operation. + * It supports phasing and counts the number of times a value was requested + * by the downstream. + * @param the value type + * @param the per-subscriber user-defined state type + */ + public static final class SubscriptionState { + private final AbstractOnSubscribe parent; + private final Subscriber subscriber; + private final S state; + private final AtomicLong requestCount; + private final AtomicInteger inUse; + private int phase; + private long calls; + private T theValue; + private boolean hasOnNext; + private boolean hasCompleted; + private boolean stopRequested; + private Throwable theException; + private SubscriptionState(AbstractOnSubscribe parent, Subscriber subscriber, S state) { + this.parent = parent; + this.subscriber = subscriber; + this.state = state; + this.requestCount = new AtomicLong(); + this.inUse = new AtomicInteger(1); + } + /** + * @return the per-subscriber specific user-defined state created via AbstractOnSubscribe.onSubscribe. + */ + public S state() { + return state; + } + /** + * @return the current phase value + */ + public int phase() { + return phase; + } + /** + * Sets a new phase value. + * @param newPhase + */ + public void phase(int newPhase) { + phase = newPhase; + } + /** + * Advance the current phase by 1. + */ + public void advancePhase() { + advancePhaseBy(1); + } + /** + * Advance the current phase by the given amount (can be negative). + * @param amount the amount to advance the phase + */ + public void advancePhaseBy(int amount) { + phase += amount; + } + /** + * @return the number of times AbstractOnSubscribe.next was called so far, starting at 0 + * for the very first call. + */ + public long calls() { + return calls; + } + /** + * Call this method to offer the next onNext value for the subscriber. + *

+ * Throws IllegalStateException if there is a value already offered but not taken or + * a terminal state is reached. + * @param value the value to onNext + */ + public void onNext(T value) { + if (hasOnNext) { + throw new IllegalStateException("onNext not consumed yet!"); + } else + if (hasCompleted) { + throw new IllegalStateException("Already terminated", theException); + } + theValue = value; + hasOnNext = true; + } + /** + * Call this method to send an onError to the subscriber and terminate + * all further activities. If there is an onNext even not taken, that + * value is emitted to the subscriber followed by this exception. + *

+ * Throws IllegalStateException if the terminal state has been reached already. + * @param e the exception to deliver to the client + */ + public void onError(Throwable e) { + if (e == null) { + throw new NullPointerException("e != null required"); + } + if (hasCompleted) { + throw new IllegalStateException("Already terminated", theException); + } + theException = e; + hasCompleted = true; + } + /** + * Call this method to send an onCompleted to the subscriber and terminate + * all further activities. If there is an onNext even not taken, that + * value is emitted to the subscriber followed by this exception. + *

+ * Throws IllegalStateException if the terminal state has been reached already. + * @param e the exception to deliver to the client + */ + public void onCompleted() { + if (hasCompleted) { + throw new IllegalStateException("Already terminated", theException); + } + hasCompleted = true; + } + /** + * Signals that there won't be any further events. + */ + public void stop() { + stopRequested = true; + } + /** + * Emits the onNextValue and/or the terminal value to the actual subscriber. + * @return true if the event was a terminal event + */ + protected boolean accept() { + if (hasOnNext) { + T value = theValue; + theValue = null; + hasOnNext = false; + + try { + subscriber.onNext(value); + } catch (Throwable t) { + hasCompleted = true; + Throwable e = theException; + theException = null; + if (e == null) { + subscriber.onError(t); + } else { + subscriber.onError(new CompositeException(Arrays.asList(t, e))); + } + return true; + } + } + if (hasCompleted) { + Throwable e = theException; + theException = null; + + if (e != null) { + subscriber.onError(e); + } else { + subscriber.onCompleted(); + } + return true; + } + return false; + } + /** + * Verify if the next() generated an event or requested a stop. + * @return true if either event was generated or stop was requested + */ + protected boolean verify() { + return hasOnNext || hasCompleted || stopRequested; + } + /** @returns true if the next() requested a stop. */ + protected boolean stopRequested() { + return stopRequested; + } + /** + * Request the state to be used by onNext or returns false if + * the downstream has unsubscribed. + * @return true if the state can be used exclusively + */ + protected boolean use() { + int i = inUse.get(); + if (i == 0) { + return false; + } else + if (i == 1 && inUse.compareAndSet(1, 2)) { + return true; + } + throw new IllegalStateException("This is not reentrant nor threadsafe!"); + } + /** + * Release the state if there are no more interest in it and + * is not in use. + */ + protected void free() { + int i = inUse.get(); + if (i <= 0) { + return; + } else + if (inUse.decrementAndGet() == 0) { + parent.onTerminated(state); + } + } + /** + * Terminates the state immediately and calls + * onTerminated with the custom state. + */ + protected void terminate() { + for (;;) { + int i = inUse.get(); + if (i <= 0) { + return; + } + if (inUse.compareAndSet(i, 0)) { + parent.onTerminated(state); + break; + } + } + } + } +} diff --git a/src/test/java/rx/observables/AbstractOnSubscribeTest.java b/src/test/java/rx/observables/AbstractOnSubscribeTest.java new file mode 100644 index 0000000000..e408a166f0 --- /dev/null +++ b/src/test/java/rx/observables/AbstractOnSubscribeTest.java @@ -0,0 +1,506 @@ +/** + * 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.observables; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.Test; +import org.mockito.InOrder; + +import rx.*; +import rx.Observable; +import rx.Observer; +import rx.exceptions.TestException; +import rx.functions.*; +import rx.observables.AbstractOnSubscribe.SubscriptionState; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; + +/** + * Test if AbstractOnSubscribe adheres to the usual unsubscription and backpressure contracts. + */ +public class AbstractOnSubscribeTest { + @Test + public void testJust() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onNext(1); + state.onCompleted(); + } + }; + + TestSubscriber ts = new TestSubscriber(); + + aos.toObservable().subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1)); + } + @Test + public void testJustMisbehaving() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onNext(1); + state.onNext(2); + state.onCompleted(); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + aos.toObservable().subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onCompleted(); + verify(o).onError(any(IllegalStateException.class)); + } + @Test + public void testJustMisbehavingOnCompleted() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onNext(1); + state.onCompleted(); + state.onCompleted(); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + aos.toObservable().subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onCompleted(); + verify(o).onError(any(IllegalStateException.class)); + } + @Test + public void testJustMisbehavingOnError() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onNext(1); + state.onError(new TestException("Forced failure 1")); + state.onError(new TestException("Forced failure 2")); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + aos.toObservable().subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onCompleted(); + verify(o).onError(any(IllegalStateException.class)); + } + @Test + public void testEmpty() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onCompleted(); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + aos.toObservable().subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onCompleted(); + } + @Test + public void testNever() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.stop(); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + aos.toObservable().subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onError(any(Throwable.class)); + verify(o, never()).onCompleted(); + } + + @Test + public void testThrows() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + throw new TestException("Forced failure"); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + aos.toObservable().subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onCompleted(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void testError() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onError(new TestException("Forced failure")); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + aos.toObservable().subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o).onError(any(TestException.class)); + verify(o, never()).onCompleted(); + } + @Test + public void testRange() { + final int start = 1; + final int count = 100; + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + long calls = state.calls(); + if (calls <= count) { + state.onNext((int)calls + start); + if (calls == count) { + state.onCompleted(); + } + } + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + aos.toObservable().subscribe(o); + + verify(o, never()).onError(any(TestException.class)); + for (int i = start; i < start + count; i++) { + inOrder.verify(o).onNext(i); + } + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + @Test + public void testFromIterable() { + int n = 100; + final List source = new ArrayList(); + for (int i = 0; i < n; i++) { + source.add(i); + } + + AbstractOnSubscribe> aos = new AbstractOnSubscribe>() { + @Override + protected Iterator onSubscribe( + Subscriber subscriber) { + return source.iterator(); + } + @Override + protected void next(SubscriptionState> state) { + Iterator it = state.state(); + if (it.hasNext()) { + state.onNext(it.next()); + } + if (!it.hasNext()) { + state.onCompleted(); + } + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + aos.toObservable().subscribe(o); + + verify(o, never()).onError(any(TestException.class)); + for (int i = 0; i < n; i++) { + inOrder.verify(o).onNext(i); + } + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testPhased() { + final int count = 100; + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + long c = state.calls(); + switch (state.phase()) { + case 0: + if (c < count) { + state.onNext("Beginning"); + if (c == count - 1) { + state.advancePhase(); + } + } + break; + case 1: + state.onNext("Beginning"); + state.advancePhase(); + break; + case 2: + state.onNext("Finally"); + state.onCompleted(); + state.advancePhase(); + break; + default: + throw new IllegalStateException("Wrong phase: " + state.phase()); + } + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + aos.toObservable().subscribe(o); + + verify(o, never()).onError(any(Throwable.class)); + inOrder.verify(o, times(count + 1)).onNext("Beginning"); + inOrder.verify(o).onNext("Finally"); + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + @Test + public void testPhasedRetry() { + final int count = 100; + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + int calls; + int phase; + @Override + protected void next(SubscriptionState state) { + switch (phase) { + case 0: + if (calls++ < count) { + state.onNext("Beginning"); + state.onError(new TestException()); + } else { + phase++; + } + break; + case 1: + state.onNext("Beginning"); + phase++; + break; + case 2: + state.onNext("Finally"); + state.onCompleted(); + phase++; + break; + default: + throw new IllegalStateException("Wrong phase: " + state.phase()); + } + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + aos.toObservable().retry(2 * count).subscribe(o); + + verify(o, never()).onError(any(Throwable.class)); + inOrder.verify(o, times(count + 1)).onNext("Beginning"); + inOrder.verify(o).onNext("Finally"); + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + @Test + public void testInfiniteTake() { + int count = 100; + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onNext((int)state.calls()); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + aos.toObservable().take(count).subscribe(o); + + verify(o, never()).onError(any(Throwable.class)); + for (int i = 0; i < 100; i++) { + inOrder.verify(o).onNext(i); + } + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + @Test + public void testInfiniteRequestSome() { + int count = 100; + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onNext((int)state.calls()); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + TestSubscriber ts = new TestSubscriber(o) { + @Override + public void onStart() { + requestMore(0); // don't start right away + } + }; + + aos.toObservable().subscribe(ts); + + ts.requestMore(count); + + verify(o, never()).onError(any(Throwable.class)); + verify(o, never()).onCompleted(); + for (int i = 0; i < count; i++) { + inOrder.verify(o).onNext(i); + } + inOrder.verifyNoMoreInteractions(); + } + @Test + public void testIndependentStates() { + int count = 100; + final ConcurrentHashMap states = new ConcurrentHashMap(); + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + states.put(state, state); + state.stop(); + } + }; + Observable source = aos.toObservable(); + for (int i = 0; i < count; i++) { + source.subscribe(); + } + + assertEquals(count, states.size()); + } + @Test(timeout = 3000) + public void testSubscribeOn() { + final int start = 1; + final int count = 100; + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + long calls = state.calls(); + if (calls <= count) { + state.onNext((int)calls + start); + if (calls == count) { + state.onCompleted(); + } + } + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + TestSubscriber ts = new TestSubscriber(o); + + aos.toObservable().subscribeOn(Schedulers.newThread()).subscribe(ts); + + ts.awaitTerminalEvent(); + + verify(o, never()).onError(any(Throwable.class)); + for (int i = 1; i <= count; i++) { + inOrder.verify(o).onNext(i); + } + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + + } + @Test(timeout = 10000) + public void testObserveOn() { + final int start = 1; + final int count = 1000; + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + long calls = state.calls(); + if (calls <= count) { + state.onNext((int)calls + start); + if (calls == count) { + state.onCompleted(); + } + } + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + TestSubscriber ts = new TestSubscriber(o); + + aos.toObservable().observeOn(Schedulers.newThread()).subscribe(ts); + + ts.awaitTerminalEvent(); + + verify(o, never()).onError(any(Throwable.class)); + verify(o, times(count + 1)).onNext(any(Integer.class)); + verify(o).onCompleted(); + + for (int i = 0; i < ts.getOnNextEvents().size(); i++) { + Object object = ts.getOnNextEvents().get(i); + assertEquals(i + 1, object); + } + } + @Test + public void testMissingEmission() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Action1> empty = Actions.empty(); + AbstractOnSubscribe.create(empty).toObservable().subscribe(o); + + verify(o, never()).onCompleted(); + verify(o, never()).onNext(any(Object.class)); + verify(o).onError(any(IllegalStateException.class)); + } +} From 053cc4f1b2104310902241954215168e1cef77be Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 9 Dec 2014 10:20:00 +0100 Subject: [PATCH 009/857] Fixed first emission racing with pre and post subscription. --- src/main/java/rx/subjects/ReplaySubject.java | 103 +++++++++++++++++- .../java/rx/subjects/BehaviorSubjectTest.java | 75 ++++++++++++- .../ReplaySubjectBoundedConcurrencyTest.java | 69 +++++++++++- .../ReplaySubjectConcurrencyTest.java | 80 ++++++++++++-- 4 files changed, 303 insertions(+), 24 deletions(-) diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index 4c83db5134..a137dd6c4d 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -102,6 +102,42 @@ public void call(SubjectObserver o) { o.index(lastIndex); } }; + ssm.onAdded = new Action1>() { + @Override + public void call(SubjectObserver o) { + synchronized (o) { + if (!o.first || o.emitting) { + return; + } + o.first = false; + o.emitting = true; + } + boolean skipFinal = false; + try { + for (;;) { + int idx = o.index(); + int sidx = state.index; + if (idx != sidx) { + Integer j = state.replayObserverFromIndex(idx, o); + o.index(j); + } + synchronized (o) { + if (sidx == state.index) { + o.emitting = false; + skipFinal = true; + break; + } + } + } + } finally { + if (!skipFinal) { + synchronized (o) { + o.emitting = false; + } + } + } + } + }; ssm.onTerminated = new Action1>() { @Override public void call(SubjectObserver o) { @@ -264,6 +300,42 @@ static final ReplaySubject createWithState(final BoundedState state, Action1> onStart) { SubjectSubscriptionManager ssm = new SubjectSubscriptionManager(); ssm.onStart = onStart; + ssm.onAdded = new Action1>() { + @Override + public void call(SubjectObserver o) { + synchronized (o) { + if (!o.first || o.emitting) { + return; + } + o.first = false; + o.emitting = true; + } + boolean skipFinal = false; + try { + for (;;) { + NodeList.Node idx = o.index(); + NodeList.Node sidx = state.tail(); + if (idx != sidx) { + NodeList.Node j = state.replayObserverFromIndex(idx, o); + o.index(j); + } + synchronized (o) { + if (sidx == state.tail()) { + o.emitting = false; + skipFinal = true; + break; + } + } + } + } finally { + if (!skipFinal) { + synchronized (o) { + o.emitting = false; + } + } + } + } + }; ssm.onTerminated = new Action1>() { @Override @@ -355,9 +427,10 @@ public boolean hasObservers() { private boolean caughtUp(SubjectObserver o) { if (!o.caughtUp) { - o.caughtUp = true; - state.replayObserver(o); - o.index(null); // once caught up, no need for the index anymore + if (state.replayObserver(o)) { + o.caughtUp = true; + o.index(null); // once caught up, no need for the index anymore + } return false; } else { // it was caught up so proceed the "raw route" @@ -423,11 +496,20 @@ public boolean terminated() { } @Override - public void replayObserver(SubjectObserver observer) { + public boolean replayObserver(SubjectObserver observer) { + + synchronized (observer) { + observer.first = false; + if (observer.emitting) { + return false; + } + } + Integer lastEmittedLink = observer.index(); if (lastEmittedLink != null) { int l = replayObserverFromIndex(lastEmittedLink, observer); observer.index(l); + return true; } else { throw new IllegalStateException("failed to find lastEmittedLink for: " + observer); } @@ -525,10 +607,18 @@ public Node tail() { return tail; } @Override - public void replayObserver(SubjectObserver observer) { + public boolean replayObserver(SubjectObserver observer) { + synchronized (observer) { + observer.first = false; + if (observer.emitting) { + return false; + } + } + NodeList.Node lastEmittedLink = observer.index(); NodeList.Node l = replayObserverFromIndex(lastEmittedLink, observer); observer.index(l); + return true; } @Override @@ -571,8 +661,9 @@ interface ReplayState { /** * Replay contents to the given observer. * @param observer the receiver of events + * @return true if the subject has caught up */ - void replayObserver(SubjectObserver observer); + boolean replayObserver(SubjectObserver observer); /** * Replay the buffered values from an index position and return a new index * @param idx the current index position diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index aa5f716481..76ae5bd723 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -25,17 +25,19 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import org.junit.Test; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; import org.mockito.InOrder; import org.mockito.Mockito; -import rx.Observable; -import rx.Observer; -import rx.Subscription; +import rx.*; import rx.exceptions.CompositeException; import rx.exceptions.OnErrorNotImplementedException; -import rx.functions.Func1; +import rx.functions.*; import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; public class BehaviorSubjectTest { @@ -417,4 +419,67 @@ public void testOnErrorThrowsDoesntPreventDelivery2() { // even though the onError above throws we should still receive it on the other subscriber assertEquals(1, ts.getOnErrorEvents().size()); } + @Test + public void testEmissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final BehaviorSubject rs = BehaviorSubject.create(); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Action0() { + @Override + public void call() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference o = new AtomicReference(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new Observer() { + + @Override + public void onCompleted() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onCompleted(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + rs.onCompleted(); + } + } + } } diff --git a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java index d086224b9c..bf4c2bbc1e 100644 --- a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java @@ -25,12 +25,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import org.junit.Test; +import org.junit.*; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; import rx.Subscriber; -import rx.functions.Action1; +import rx.functions.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -337,4 +337,67 @@ public void run() { } } } + @Test + public void testReplaySubjectEmissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final ReplaySubject rs = ReplaySubject.createWithSize(2); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Action0() { + @Override + public void call() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference o = new AtomicReference(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new Observer() { + + @Override + public void onCompleted() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onCompleted(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + rs.onCompleted(); + } + } + } } \ No newline at end of file diff --git a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java index 4c7c947745..304533e012 100644 --- a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java @@ -17,20 +17,17 @@ import static org.junit.Assert.assertEquals; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; -import org.junit.Test; +import org.junit.*; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.functions.Action1; +import rx.Observable; +import rx.Observer; +import rx.functions.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -337,4 +334,67 @@ public void run() { } } } + @Test + public void testReplaySubjectEmissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final ReplaySubject rs = ReplaySubject.create(); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Action0() { + @Override + public void call() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference o = new AtomicReference(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new Observer() { + + @Override + public void onCompleted() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onCompleted(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + rs.onCompleted(); + } + } + } } From b21aef940e7575f5fb9e2f995cf60eb7d4fb5658 Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 9 Dec 2014 08:17:17 -0800 Subject: [PATCH 010/857] minor tweaks to javadocs for new onBackpressureBuffer variants --- src/main/java/rx/Observable.java | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 98e83775a2..126196c516 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5037,10 +5037,10 @@ public final Observable onBackpressureBuffer() { } /** - * 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 {@code onError} emitting a - * {@link java.nio.BufferOverflowException} as soon as the buffer's capacity is exceeded, dropping all - * undelivered items, and unsubscribing from the source. + * 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 {@code onError} emitting + * a {@code BufferOverflowException} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, and unsubscribing from the source. *

* *

@@ -5048,9 +5048,10 @@ 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. + * @return the source Observable modified to buffer items up to the given capacity * @see RxJava wiki: Backpressure * @Beta + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta public final Observable onBackpressureBuffer(long capacity) { @@ -5058,10 +5059,10 @@ public final Observable onBackpressureBuffer(long capacity) { } /** - * 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 {@code onError} emitting a - * {@link java.nio.BufferOverflowException} as soon as the buffer's capacity is exceeded, dropping all - * undelivered items, unsubscribing from the source, and notifying the producer with {@code 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 {@code onError} emitting + * a {@code BufferOverflowException} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, unsubscribing from the source, and notifying the producer with {@code onOverflow}. *

* *

@@ -5069,8 +5070,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. + * @return the source Observable modified to buffer items up to the given capacity * @see RxJava wiki: Backpressure + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) * @Beta */ @Beta From b02e572bdb781485ce87181f70b426454ab8abcb Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 9 Dec 2014 10:59:29 -0800 Subject: [PATCH 011/857] tidying up AbstractOnSubscribe javadocs --- .../rx/observables/AbstractOnSubscribe.java | 245 +++++++++++------- 1 file changed, 148 insertions(+), 97 deletions(-) diff --git a/src/main/java/rx/observables/AbstractOnSubscribe.java b/src/main/java/rx/observables/AbstractOnSubscribe.java index 7cbb46b8a1..6dbe7ad44a 100644 --- a/src/main/java/rx/observables/AbstractOnSubscribe.java +++ b/src/main/java/rx/observables/AbstractOnSubscribe.java @@ -26,58 +26,65 @@ import rx.functions.*; /** - * Abstract base class for the OnSubscribe interface that helps building - * observable sources one onNext at a time and automatically supports - * unsubscription and backpressure. + * Abstract base class for the {@link OnSubscribe} interface that helps you build Observable sources one + * {@code onNext} at a time, and automatically supports unsubscription and backpressure. *

*

Usage rules

- * Implementors of the {@code next()} method + * When you implement the {@code next()} method, you *
    - *
  • should either + *
  • should either *
      - *
    • create the next value and signal it via {@code state.onNext()},
    • - *
    • signal a terminal condition via {@code state.onError()} or {@code state.onCompleted()} or
    • - *
    • signal a stop condition via {@code state.stop()} indicating no further values will be sent.
    • + *
    • create the next value and signal it via {@link SubscriptionState#onNext state.onNext()},
    • + *
    • signal a terminal condition via {@link SubscriptionState#onError state.onError()}, or + * {@link SubscriptionState#onCompleted state.onCompleted()}, or
    • + *
    • signal a stop condition via {@link SubscriptionState#stop state.stop()} indicating no further values + * will be sent.
    • *
    - *
  • - *
  • may + *
  • + *
  • may *
      - *
    • call {@code state.onNext()} and either {@code state.onError()} or {@code state.onCompleted()} together and + *
    • call {@link SubscriptionState#onNext state.onNext()} and either + * {@link SubscriptionState#onError state.onError()} or + * {@link SubscriptionState#onCompleted state.onCompleted()} together, and *
    • block or sleep. *
    - *
  • - *
  • should not + *
  • + *
  • should not *
      *
    • do nothing or do async work and not produce any event or request stopping. If neither of - * the methods are called, an {@code IllegalStateException} is forwarded to the {@code Subscriber} and - * the Observable is terminated;
    • - *
    • call the {@code state.onXYZ} methods more than once (yields {@code IllegalStateException}).
    • + * the methods are called, an {@link IllegalStateException} is forwarded to the {@code Subscriber} and + * the Observable is terminated; + *
    • call the {@code state.on}foo() methods more than once (yields + * {@link IllegalStateException}).
    • *
    - *
  • + * *
* - * The {@code SubscriptionState} object features counters that may help implement a state machine: + * The {@link SubscriptionState} object features counters that may help implement a state machine: *
    - *
  • A call counter, accessible via {@code state.calls()} that tells how many times - * the {@code next()} was run (zero based). - *
  • A phase counter, accessible via {@code state.phase()} that helps track the current emission - * phase and may be used in a {@code switch ()} statement to implement the state machine. - * (It was named phase to avoid confusion with the per-subscriber state.)
  • - *
  • The current phase can be arbitrarily changed via {@code state.advancePhase()}, - * {@code state.advancePhaseBy(int)} and {@code state.phase(int)}.
  • - * + *
  • A call counter, accessible via {@link SubscriptionState#calls state.calls()} tells how many times the + * {@code next()} was run (zero based).
  • + *
  • You can use a phase counter, accessible via {@link SubscriptionState#phase state.phase}, that helps track + * the current emission phase, in a {@code switch()} statement to implement the state machine. (It is named + * {@code phase} to avoid confusion with the per-subscriber state.)
  • + *
  • You can arbitrarily change the current phase with + * {@link SubscriptionState#advancePhase state.advancePhase()}, + * {@link SubscriptionState#advancePhaseBy(int) state.advancedPhaseBy(int)} and + * {@link SubscriptionState#phase(int) state.phase(int)}.
  • *
*

- * The implementors of the {@code AbstractOnSubscribe} may override the {@code onSubscribe} to perform - * special actions (such as registering {@code Subscription}s with {@code Subscriber.add()}) and return additional state for each subscriber subscribing. This custom state is - * accessible through the {@code state.state()} method. If the custom state requires some form of cleanup, - * the {@code onTerminated} method can be overridden. + * When you implement {@code AbstractOnSubscribe}, you may override {@link AbstractOnSubscribe#onSubscribe} to + * perform special actions (such as registering {@code Subscription}s with {@code Subscriber.add()}) and return + * additional state for each subscriber subscribing. You can access this custom state with the + * {@link SubscriptionState#state state.state()} method. If you need to do some cleanup, you can override the + * {@link #onTerminated} method. *

- * For convenience, lambda-accepting static factory methods, named {@code create()}, are available. Another - * convenience is the {@code toObservable} which turns an {@code AbstractOnSubscribe} instance into an {@code Observable} fluently. + * For convenience, a lambda-accepting static factory method, {@link #create}, is available. + * Another convenience is {@link #toObservable} which turns an {@code AbstractOnSubscribe} + * instance into an {@code Observable} fluently. * *

Examples

- * Note: the examples use the lambda-helper factories to avoid boilerplane. + * Note: these examples use the lambda-helper factories to avoid boilerplane. * *

Implement: just

*

@@ -100,7 +107,7 @@
  *   }
  * }, u -> iterable.iterator()).subscribe(System.out::println);
  * 
- + * *

Implement source that fails a number of times before succeeding

*

  * AtomicInteger fails = new AtomicInteger();
@@ -136,37 +143,43 @@
  * .timeout(1, TimeUnit.SECONDS)
  * .subscribe(System.out::println, Throwable::printStacktrace, () -> System.out.println("Done"));
  * 
- + * * @param the value type * @param the per-subscriber user-defined state type + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @Experimental */ @Experimental public abstract class AbstractOnSubscribe implements OnSubscribe { /** - * Called when a Subscriber subscribes and let's the implementor - * create a per-subscriber custom state. + * Called when a Subscriber subscribes and lets the implementor create a per-subscriber custom state. *

- * Override this method to have custom state per-subscriber. - * The default implementation returns {@code null}. + * Override this method to have custom state per-subscriber. The default implementation returns + * {@code null}. + * * @param subscriber the subscriber who is subscribing * @return the custom state */ protected S onSubscribe(Subscriber subscriber) { return null; } + /** * Called after the terminal emission or when the downstream unsubscribes. *

- * This is called only once and it is made sure no onNext call runs concurrently with it. - * The default implementation does nothing. + * This is called only once and no {@code onNext} call will run concurrently with it. The default + * implementation does nothing. + * * @param state the user-provided state */ protected void onTerminated(S state) { } + /** - * Override this method and create an emission state-machine. - * @param state the per-subscriber subscription state. + * Override this method to create an emission state-machine. + * + * @param state the per-subscriber subscription state */ protected abstract void next(SubscriptionState state); @@ -179,7 +192,8 @@ public final void call(final Subscriber subscriber) { } /** - * Convenience method to create an observable from the implemented instance + * Convenience method to create an Observable from this implemented instance. + * * @return the created observable */ public final Observable toObservable() { @@ -195,14 +209,15 @@ public Object call(Object t1) { }; /** - * Creates an AbstractOnSubscribe instance which calls the provided {@code next} action. + * Creates an {@code AbstractOnSubscribe} instance which calls the provided {@code next} action. *

- * This is a convenience method to help create AbstractOnSubscribe instances with the - * help of lambdas. + * This is a convenience method to help create {@code AbstractOnSubscribe} instances with the help of + * lambdas. + * * @param the value type * @param the per-subscriber user-defined state type * @param next the next action to call - * @return an AbstractOnSubscribe instance + * @return an {@code AbstractOnSubscribe} instance */ public static AbstractOnSubscribe create(Action1> next) { @SuppressWarnings("unchecked") @@ -210,42 +225,49 @@ public static AbstractOnSubscribe create(Action1, ? extends S>)NULL_FUNC1; return create(next, nullFunc, Actions.empty()); } + /** - * Creates an AbstractOnSubscribe instance which creates a custom state with the - * {@code onSubscribe} function and calls the provided {@code next} action. + * Creates an {@code AbstractOnSubscribe} instance which creates a custom state with the {@code onSubscribe} + * function and calls the provided {@code next} action. *

- * This is a convenience method to help create AbstractOnSubscribe instances with the - * help of lambdas. + * This is a convenience method to help create {@code AbstractOnSubscribe} instances with the help of + * lambdas. + * * @param the value type * @param the per-subscriber user-defined state type * @param next the next action to call - * @param onSubscribe the function that returns a per-subscriber state to be used by next - * @return an AbstractOnSubscribe instance + * @param onSubscribe the function that returns a per-subscriber state to be used by {@code next} + * @return an {@code AbstractOnSubscribe} instance */ public static AbstractOnSubscribe create(Action1> next, Func1, ? extends S> onSubscribe) { return create(next, onSubscribe, Actions.empty()); } + /** - * Creates an AbstractOnSubscribe instance which creates a custom state with the - * {@code onSubscribe} function, calls the provided {@code next} action and - * calls the {@code onTerminated} action to release the state when its no longer needed. + * Creates an {@code AbstractOnSubscribe} instance which creates a custom state with the {@code onSubscribe} + * function, calls the provided {@code next} action and calls the {@code onTerminated} action to release the + * state when its no longer needed. *

- * This is a convenience method to help create AbstractOnSubscribe instances with the - * help of lambdas. + * This is a convenience method to help create {@code AbstractOnSubscribe} instances with the help of + * lambdas. + * * @param the value type * @param the per-subscriber user-defined state type * @param next the next action to call - * @param onSubscribe the function that returns a per-subscriber state to be used by next - * @param onTerminated the action to call to release the state created by the onSubscribe function - * @return an AbstractOnSubscribe instance + * @param onSubscribe the function that returns a per-subscriber state to be used by {@code next} + * @param onTerminated the action to call to release the state created by the {@code onSubscribe} function + * @return an {@code AbstractOnSubscribe} instance */ public static AbstractOnSubscribe create(Action1> next, Func1, ? extends S> onSubscribe, Action1 onTerminated) { return new LambdaOnSubscribe(next, onSubscribe, onTerminated); } + /** - * An implementation that forwards the 3 main methods to functional callbacks. + * An implementation that forwards the three main methods ({@code next}, {@code onSubscribe}, and + * {@code onTermianted}) to functional callbacks. + * * @param the value type * @param the per-subscriber user-defined state type */ @@ -272,13 +294,14 @@ protected void next(SubscriptionState state) { next.call(state); } } + /** * Manages unsubscription of the state. + * * @param the value type * @param the per-subscriber user-defined state type */ private static final class SubscriptionCompleter extends AtomicBoolean implements Subscription { - /** */ private static final long serialVersionUID = 7993888274897325004L; private final SubscriptionState state; private SubscriptionCompleter(SubscriptionState state) { @@ -298,6 +321,7 @@ public void unsubscribe() { } /** * Contains the producer loop that reacts to downstream requests of work. + * * @param the value type * @param the per-subscriber user-defined state type */ @@ -325,9 +349,11 @@ public void request(long n) { } } } + /** * Executes the user-overridden next() method and performs state bookkeeping and * verification. + * * @return true if the outer loop may continue */ protected boolean doNext() { @@ -355,12 +381,15 @@ protected boolean doNext() { return false; } } + /** - * Represents a per-subscription state for the AbstractOnSubscribe operation. - * It supports phasing and counts the number of times a value was requested - * by the downstream. + * Represents a per-subscription state for the {@code AbstractOnSubscribe} operation. It supports phasing + * and counts the number of times a value was requested by the downstream. + * * @param the value type * @param the per-subscriber user-defined state type + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @Experimental */ public static final class SubscriptionState { private final AbstractOnSubscribe parent; @@ -382,51 +411,61 @@ private SubscriptionState(AbstractOnSubscribe parent, Subscriber - * Throws IllegalStateException if there is a value already offered but not taken or - * a terminal state is reached. - * @param value the value to onNext + * Call this method to offer the next {@code onNext} value for the subscriber. + * + * @param value the value to {@code onNext} + * @throws IllegalStateException if there is a value already offered but not taken or a terminal state + * is reached */ public void onNext(T value) { if (hasOnNext) { @@ -438,13 +477,14 @@ public void onNext(T value) { theValue = value; hasOnNext = true; } + /** - * Call this method to send an onError to the subscriber and terminate - * all further activities. If there is an onNext even not taken, that - * value is emitted to the subscriber followed by this exception. - *

- * Throws IllegalStateException if the terminal state has been reached already. + * Call this method to send an {@code onError} to the subscriber and terminate all further activities. + * If there is a pending {@code onNext}, that value is emitted to the subscriber followed by this + * exception. + * * @param e the exception to deliver to the client + * @throws IllegalStateException if the terminal state has been reached already */ public void onError(Throwable e) { if (e == null) { @@ -456,13 +496,13 @@ public void onError(Throwable e) { theException = e; hasCompleted = true; } + /** - * Call this method to send an onCompleted to the subscriber and terminate - * all further activities. If there is an onNext even not taken, that - * value is emitted to the subscriber followed by this exception. - *

- * Throws IllegalStateException if the terminal state has been reached already. - * @param e the exception to deliver to the client + * Call this method to send an {@code onCompleted} to the subscriber and terminate all further + * activities. If there is a pending {@code onNext}, that value is emitted to the subscriber followed by + * this exception. + * + * @throws IllegalStateException if the terminal state has been reached already */ public void onCompleted() { if (hasCompleted) { @@ -470,15 +510,18 @@ public void onCompleted() { } hasCompleted = true; } + /** * Signals that there won't be any further events. */ public void stop() { stopRequested = true; } + /** - * Emits the onNextValue and/or the terminal value to the actual subscriber. - * @return true if the event was a terminal event + * Emits the {@code onNext} and/or the terminal value to the actual subscriber. + * + * @return {@code true} if the event was a terminal event */ protected boolean accept() { if (hasOnNext) { @@ -513,21 +556,28 @@ protected boolean accept() { } return false; } + /** - * Verify if the next() generated an event or requested a stop. + * Verify if the {@code next()} generated an event or requested a stop. + * * @return true if either event was generated or stop was requested */ protected boolean verify() { return hasOnNext || hasCompleted || stopRequested; } - /** @returns true if the next() requested a stop. */ + + /** @return true if the {@code next()} requested a stop */ protected boolean stopRequested() { return stopRequested; } + /** - * Request the state to be used by onNext or returns false if - * the downstream has unsubscribed. - * @return true if the state can be used exclusively + * Request the state to be used by {@code onNext} or returns {@code false} if the downstream has + * unsubscribed. + * + * @return {@code true} if the state can be used exclusively + * @throws IllegalStateEception + * @warn "throws" section incomplete */ protected boolean use() { int i = inUse.get(); @@ -539,9 +589,9 @@ protected boolean use() { } throw new IllegalStateException("This is not reentrant nor threadsafe!"); } + /** - * Release the state if there are no more interest in it and - * is not in use. + * Release the state if there are no more interest in it and it is not in use. */ protected void free() { int i = inUse.get(); @@ -552,9 +602,10 @@ protected void free() { parent.onTerminated(state); } } + /** - * Terminates the state immediately and calls - * onTerminated with the custom state. + * Terminates the state immediately and calls {@link AbstractOnSubscribe#onTerminated} with the custom + * state. */ protected void terminate() { for (;;) { From 4c16adf9b06c2c84e94f1269266805aed1e54b9d Mon Sep 17 00:00:00 2001 From: zsxwing Date: Tue, 9 Dec 2014 14:33:38 +0800 Subject: [PATCH 012/857] Add "Subscriptions.unsubscribed" to fix the 'isUnsubscribed' issue --- src/main/java/rx/Observable.java | 4 +- .../OperatorTimeoutWithSelector.java | 6 +-- .../internal/schedulers/NewThreadWorker.java | 2 +- .../rx/schedulers/CachedThreadScheduler.java | 2 +- .../rx/schedulers/EventLoopsScheduler.java | 2 +- .../java/rx/schedulers/ExecutorScheduler.java | 4 +- .../rx/schedulers/ImmediateScheduler.java | 2 +- .../rx/schedulers/TrampolineScheduler.java | 4 +- .../MultipleAssignmentSubscription.java | 2 +- .../subscriptions/RefCountSubscription.java | 2 +- .../rx/subscriptions/SerialSubscription.java | 2 +- .../java/rx/subscriptions/Subscriptions.java | 41 +++++++++++++++---- .../rx/subscriptions/SubscriptionsTest.java | 16 ++++++++ 13 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 126196c516..f151c690ef 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7305,7 +7305,7 @@ public final Subscription unsafeSubscribe(Subscriber subscriber) { // TODO why aren't we throwing the hook's return value. throw r; } - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } } @@ -7394,7 +7394,7 @@ public final Subscription subscribe(Subscriber subscriber) { // TODO why aren't we throwing the hook's return value. throw r; } - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } } diff --git a/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java b/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java index 6295a35fbf..ce201c3c26 100644 --- a/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java +++ b/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java @@ -51,7 +51,7 @@ public Subscription call( } catch (Throwable t) { Exceptions.throwIfFatal(t); timeoutSubscriber.onError(t); - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } return o.unsafeSubscribe(new Subscriber() { @@ -72,7 +72,7 @@ public void onNext(U t) { }); } else { - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } } }, new TimeoutStub() { @@ -87,7 +87,7 @@ public Subscription call( } catch (Throwable t) { Exceptions.throwIfFatal(t); timeoutSubscriber.onError(t); - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } return o.unsafeSubscribe(new Subscriber() { diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index 5354999055..ea80c651c3 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -58,7 +58,7 @@ public Subscription schedule(final Action0 action) { @Override public Subscription schedule(final Action0 action, long delayTime, TimeUnit unit) { if (isUnsubscribed) { - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } return scheduleActual(action, delayTime, unit); } diff --git a/src/main/java/rx/schedulers/CachedThreadScheduler.java b/src/main/java/rx/schedulers/CachedThreadScheduler.java index f8ade61871..fe70a58a43 100644 --- a/src/main/java/rx/schedulers/CachedThreadScheduler.java +++ b/src/main/java/rx/schedulers/CachedThreadScheduler.java @@ -141,7 +141,7 @@ public Subscription schedule(Action0 action) { public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { if (innerSubscription.isUnsubscribed()) { // don't schedule, we are unsubscribed - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } ScheduledAction s = threadWorker.scheduleActual(action, delayTime, unit); diff --git a/src/main/java/rx/schedulers/EventLoopsScheduler.java b/src/main/java/rx/schedulers/EventLoopsScheduler.java index 220e31946a..004fbeea78 100644 --- a/src/main/java/rx/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/schedulers/EventLoopsScheduler.java @@ -95,7 +95,7 @@ public Subscription schedule(Action0 action) { public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { if (innerSubscription.isUnsubscribed()) { // don't schedule, we are unsubscribed - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } ScheduledAction s = poolWorker.scheduleActual(action, delayTime, unit); diff --git a/src/main/java/rx/schedulers/ExecutorScheduler.java b/src/main/java/rx/schedulers/ExecutorScheduler.java index 2acd490f04..3f8c8899a6 100644 --- a/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/src/main/java/rx/schedulers/ExecutorScheduler.java @@ -71,7 +71,7 @@ public ExecutorSchedulerWorker(Executor executor) { @Override public Subscription schedule(Action0 action) { if (isUnsubscribed()) { - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } ExecutorAction ea = new ExecutorAction(action, tasks); tasks.add(ea); @@ -106,7 +106,7 @@ public Subscription schedule(final Action0 action, long delayTime, TimeUnit unit return schedule(action); } if (isUnsubscribed()) { - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } ScheduledExecutorService service; if (executor instanceof ScheduledExecutorService) { diff --git a/src/main/java/rx/schedulers/ImmediateScheduler.java b/src/main/java/rx/schedulers/ImmediateScheduler.java index 55997916d5..4b9c27787f 100644 --- a/src/main/java/rx/schedulers/ImmediateScheduler.java +++ b/src/main/java/rx/schedulers/ImmediateScheduler.java @@ -56,7 +56,7 @@ public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { @Override public Subscription schedule(Action0 action) { action.call(); - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } @Override diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index 9b4aa3f029..a948276cc6 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -68,7 +68,7 @@ public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { private Subscription enqueue(Action0 action, long execTime) { if (innerSubscription.isUnsubscribed()) { - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } final TimedAction timedAction = new TimedAction(action, execTime, COUNTER_UPDATER.incrementAndGet(TrampolineScheduler.this)); queue.add(timedAction); @@ -81,7 +81,7 @@ private Subscription enqueue(Action0 action, long execTime) { polled.action.call(); } } while (wip.decrementAndGet() > 0); - return Subscriptions.empty(); + 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() { diff --git a/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java b/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java index daa3ada670..e554e0aebe 100644 --- a/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java +++ b/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java @@ -26,7 +26,7 @@ */ public final class MultipleAssignmentSubscription implements Subscription { /** The shared empty state. */ - static final State EMPTY_STATE = new State(false, Subscriptions.empty()); + static final State EMPTY_STATE = new State(false, Subscriptions.unsubscribed()); volatile State state = EMPTY_STATE; static final AtomicReferenceFieldUpdater STATE_UPDATER = AtomicReferenceFieldUpdater.newUpdater(MultipleAssignmentSubscription.class, State.class, "state"); diff --git a/src/main/java/rx/subscriptions/RefCountSubscription.java b/src/main/java/rx/subscriptions/RefCountSubscription.java index d06966baa2..af225fa1a7 100644 --- a/src/main/java/rx/subscriptions/RefCountSubscription.java +++ b/src/main/java/rx/subscriptions/RefCountSubscription.java @@ -80,7 +80,7 @@ public Subscription get() { do { oldState = state; if (oldState.isUnsubscribed) { - return Subscriptions.empty(); + return Subscriptions.unsubscribed(); } else { newState = oldState.addChild(); } diff --git a/src/main/java/rx/subscriptions/SerialSubscription.java b/src/main/java/rx/subscriptions/SerialSubscription.java index 56b41ff208..4f9acdcdc7 100644 --- a/src/main/java/rx/subscriptions/SerialSubscription.java +++ b/src/main/java/rx/subscriptions/SerialSubscription.java @@ -24,7 +24,7 @@ * the previous underlying subscription to be unsubscribed. */ public final class SerialSubscription implements Subscription { - static final State EMPTY_STATE = new State(false, Subscriptions.empty()); + static final State EMPTY_STATE = new State(false, Subscriptions.unsubscribed()); volatile State state = EMPTY_STATE; static final AtomicReferenceFieldUpdater STATE_UPDATER = AtomicReferenceFieldUpdater.newUpdater(SerialSubscription.class, State.class, "state"); diff --git a/src/main/java/rx/subscriptions/Subscriptions.java b/src/main/java/rx/subscriptions/Subscriptions.java index 189f6ff0b5..e3e97ea61b 100644 --- a/src/main/java/rx/subscriptions/Subscriptions.java +++ b/src/main/java/rx/subscriptions/Subscriptions.java @@ -19,6 +19,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import rx.Subscription; +import rx.annotations.Experimental; import rx.functions.Action0; import rx.functions.Actions; @@ -30,12 +31,38 @@ private Subscriptions() { throw new IllegalStateException("No instances!"); } /** - * Returns a {@link Subscription} that does nothing. - * - * @return a {@link Subscription} that does nothing + * Returns a {@link Subscription} that unsubscribe does nothing except changing + * isUnsubscribed to true. It's stateful and isUnsubscribed + * indicates if unsubscribe is called, which is different from {@link #unsubscribed()}. + * + *


+     * Subscription empty = Subscriptions.empty();
+     * System.out.println(empty.isUnsubscribed()); // false
+     * empty.unsubscribe();
+     * System.out.println(empty.isUnsubscribed()); // true
+     * 
+ * + * @return a {@link Subscription} that unsubscribe does nothing except changing + * isUnsubscribed to true. */ public static Subscription empty() { - return EMPTY; + return BooleanSubscription.create(); + } + + /** + * Returns a {@link Subscription} that unsubscribe does nothing but is already unsubscribed. + * Its isUnsubscribed always return true, which is different from {@link #empty()}. + * + *

+     * Subscription unsubscribed = Subscriptions.unsubscribed();
+     * System.out.println(unsubscribed.isUnsubscribed()); // true
+     * 
+ * + * @return a {@link Subscription} that unsubscribe does nothing but is already unsubscribed. + */ + @Experimental + public static Subscription unsubscribed() { + return UNSUBSCRIBED; } /** @@ -124,16 +151,16 @@ public static CompositeSubscription from(Subscription... subscriptions) { /** * A {@link Subscription} that does nothing when its unsubscribe method is called. */ - private static final Empty EMPTY = new Empty(); + private static final Unsubscribed UNSUBSCRIBED = new Unsubscribed(); /** Naming classes helps with debugging. */ - private static final class Empty implements Subscription { + private static final class Unsubscribed implements Subscription { @Override public void unsubscribe() { } @Override public boolean isUnsubscribed() { - return false; + return true; } } } diff --git a/src/test/java/rx/subscriptions/SubscriptionsTest.java b/src/test/java/rx/subscriptions/SubscriptionsTest.java index 767985ddfe..b780942323 100644 --- a/src/test/java/rx/subscriptions/SubscriptionsTest.java +++ b/src/test/java/rx/subscriptions/SubscriptionsTest.java @@ -15,6 +15,8 @@ */ package rx.subscriptions; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -35,4 +37,18 @@ public void testUnsubscribeOnlyOnce() { subscription.unsubscribe(); verify(unsubscribe, times(1)).call(); } + + @Test + public void testEmpty() { + Subscription empty = Subscriptions.empty(); + assertFalse(empty.isUnsubscribed()); + empty.unsubscribe(); + assertTrue(empty.isUnsubscribed()); + } + + @Test + public void testUnsubscribed() { + Subscription unsubscribed = Subscriptions.unsubscribed(); + assertTrue(unsubscribed.isUnsubscribed()); + } } From 332b67c89a5ec8b2b3d68d008f2ca15953134989 Mon Sep 17 00:00:00 2001 From: George Campbell Date: Sat, 6 Dec 2014 13:07:37 -0800 Subject: [PATCH 013/857] Remove extraneous request(n) and onCompleted() calls when unsubscribed. --- .../java/rx/internal/operators/OnSubscribeFromIterable.java | 4 +++- src/main/java/rx/internal/operators/OperatorMerge.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index fce80c00a9..500f5a34a7 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -98,7 +98,9 @@ public void request(long n) { } if (!it.hasNext()) { - o.onCompleted(); + if (!o.isUnsubscribed()) { + o.onCompleted(); + } return; } if (REQUESTED_UPDATER.addAndGet(this, -r) == 0) { diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 9f3f36390b..6584f4fac6 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -186,7 +186,8 @@ private void handleNewSource(Observable t) { InnerSubscriber i = new InnerSubscriber(this, producerIfNeeded); i.sindex = childrenSubscribers.add(i); t.unsafeSubscribe(i); - request(1); + if (!isUnsubscribed()) + request(1); } private void handleScalarSynchronousObservable(ScalarSynchronousObservable t) { From 7337bb8f4fb2bcc49404e2fa96afb5f5e1d2cfa5 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Thu, 11 Dec 2014 20:09:46 +0800 Subject: [PATCH 014/857] Remove ActionSubscription --- .../java/rx/subscriptions/Subscriptions.java | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/src/main/java/rx/subscriptions/Subscriptions.java b/src/main/java/rx/subscriptions/Subscriptions.java index 189f6ff0b5..86dd9c78a6 100644 --- a/src/main/java/rx/subscriptions/Subscriptions.java +++ b/src/main/java/rx/subscriptions/Subscriptions.java @@ -46,38 +46,8 @@ public static Subscription empty() { * @return {@link Subscription} */ public static Subscription create(final Action0 unsubscribe) { - return new ActionSubscription(unsubscribe); + return BooleanSubscription.create(unsubscribe); } - /** - * Subscription that delegates the unsubscription action to an Action0 instance - */ - private static final class ActionSubscription implements Subscription { - volatile Action0 actual; - static final AtomicReferenceFieldUpdater ACTUAL_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(ActionSubscription.class, Action0.class, "actual"); - public ActionSubscription(Action0 action) { - this.actual = action != null ? action : Actions.empty(); - } - @Override - public boolean isUnsubscribed() { - return actual == UNSUBSCRIBED_ACTION; - } - @Override - public void unsubscribe() { - Action0 a = ACTUAL_UPDATER.getAndSet(this, UNSUBSCRIBED_ACTION); - a.call(); - } - /** The no-op unique action indicating an unsubscribed state. */ - private static final Unsubscribed UNSUBSCRIBED_ACTION = new Unsubscribed(); - /** Naming classes helps with debugging. */ - private static final class Unsubscribed implements Action0 { - @Override - public void call() { - - } - } - } - /** * Converts a {@link Future} into a {@link Subscription} and cancels it when unsubscribed. From aeea9789294de4329b21abc3b433ba454877317a Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 11 Dec 2014 16:15:12 +0100 Subject: [PATCH 015/857] Fixed timer cast-to-int crash causing incorrect benchmark. --- .../rx/operators/OperatorSerializePerf.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/perf/java/rx/operators/OperatorSerializePerf.java b/src/perf/java/rx/operators/OperatorSerializePerf.java index d838110893..49c32eca5b 100644 --- a/src/perf/java/rx/operators/OperatorSerializePerf.java +++ b/src/perf/java/rx/operators/OperatorSerializePerf.java @@ -17,20 +17,13 @@ import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Benchmark; -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.State; +import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.jmh.InputWithIncrementingInteger; -import rx.jmh.LatchedObserver; +import rx.functions.Func1; +import rx.jmh.*; import rx.schedulers.Schedulers; @BenchmarkMode(Mode.Throughput) @@ -81,7 +74,7 @@ public void call(Subscriber s) { } @State(Scope.Thread) - public static class InputWithInterval extends InputWithIncrementingInteger { + public static class InputWithInterval extends InputWithIncrementingInteger implements Func1 { @Param({ "1", "1000" }) public int size; @@ -97,7 +90,11 @@ public int getSize() { public void setup(Blackhole bh) { super.setup(bh); - interval = Observable.timer(0, 1, TimeUnit.MILLISECONDS).take(size).cast(Integer.class); + interval = Observable.timer(0, 1, TimeUnit.MILLISECONDS).take(size).map(this); + } + @Override + public Integer call(Long t1) { + return t1.intValue(); } } From c38a7806224772d508a8302c0fd3ae85d0ea5957 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 11 Dec 2014 21:53:45 +0100 Subject: [PATCH 016/857] Fixed race & late termination condition. --- .../OperatorOnBackpressureBlock.java | 38 ++++++-- .../operators/OnBackpressureBlockTest.java | 93 ++++++++++++++++++- 2 files changed, 117 insertions(+), 14 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java index bb328788e9..e106bac70d 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java @@ -58,6 +58,9 @@ void init() { child.setProducer(new Producer() { @Override public void request(long n) { + if (n == 0) { + return; + } synchronized (BlockingSubscriber.this) { if (n == Long.MAX_VALUE || requestedCount == Long.MAX_VALUE) { requestedCount = Long.MAX_VALUE; @@ -95,28 +98,41 @@ public void onCompleted() { } void drain() { long n; + boolean term; synchronized (this) { if (emitting) { return; } emitting = true; n = requestedCount; + term = terminated; } boolean skipFinal = false; try { + Subscriber child = this.child; + BlockingQueue queue = this.queue; while (true) { int emitted = 0; - while (n > 0) { - Object o = queue.poll(); - if (o == null) { - if (terminated) { - if (exception != null) { - child.onError(exception); + while (n > 0 || term) { + Object o; + if (term) { + o = queue.peek(); + if (o == null) { + Throwable e = exception; + if (e != null) { + child.onError(e); } else { child.onCompleted(); } + skipFinal = true; return; } + if (n == 0) { + break; + } + } + o = queue.poll(); + if (o == null) { break; } else { child.onNext(nl.getValue(o)); @@ -125,23 +141,25 @@ void drain() { } } synchronized (this) { + term = terminated; + boolean more = queue.peek() != null; // if no backpressure below if (requestedCount == Long.MAX_VALUE) { // no new data arrived since the last poll - if (queue.peek() == null) { + if (!more && !term) { skipFinal = true; emitting = false; return; } n = Long.MAX_VALUE; } else { - if (emitted == 0) { + requestedCount -= emitted; + n = requestedCount; + if ((n == 0 || !more) && (!term || more)) { skipFinal = true; emitting = false; return; } - requestedCount -= emitted; - n = requestedCount; } } } diff --git a/src/test/java/rx/internal/operators/OnBackpressureBlockTest.java b/src/test/java/rx/internal/operators/OnBackpressureBlockTest.java index b247b15e6b..47d3cebd71 100644 --- a/src/test/java/rx/internal/operators/OnBackpressureBlockTest.java +++ b/src/test/java/rx/internal/operators/OnBackpressureBlockTest.java @@ -16,11 +16,13 @@ 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.Mockito.*; import java.util.Arrays; - -import static org.junit.Assert.*; +import java.util.Collections; import org.junit.Test; @@ -34,6 +36,7 @@ import rx.observers.TestObserver; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; /** * Test the onBackpressureBlock() behavior. @@ -161,13 +164,15 @@ public void onStart() { Thread.sleep(WAIT); o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - o.assertNoErrors(); - assertTrue(o.getOnCompletedEvents().isEmpty()); + o.assertTerminalEvent(); + assertEquals(1, o.getOnErrorEvents().size()); + assertTrue(o.getOnErrorEvents().get(0) instanceof TestException); o.requestMore(10); Thread.sleep(WAIT); + o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); o.assertTerminalEvent(); assertEquals(1, o.getOnErrorEvents().size()); assertTrue(o.getOnErrorEvents().get(0) instanceof TestException); @@ -259,4 +264,84 @@ public void testTakeWorksSubscriberRequestUnlimitedBufferedException() { o.assertNoErrors(); o.assertTerminalEvent(); } + @Test(timeout = 10000) + public void testOnCompletedDoesntWaitIfNoEvents() { + + TestSubscriber o = new TestSubscriber() { + @Override + public void onStart() { + request(0); // make sure it doesn't start in unlimited mode + } + }; + Observable.empty().onBackpressureBlock(2).subscribe(o); + + o.assertNoErrors(); + o.assertTerminalEvent(); + o.assertReceivedOnNext(Collections.emptyList()); + } + @Test(timeout = 10000) + public void testOnCompletedDoesWaitIfEvents() { + + TestSubscriber o = new TestSubscriber() { + @Override + public void onStart() { + request(0); // make sure it doesn't start in unlimited mode + } + }; + Observable.just(1).onBackpressureBlock(2).subscribe(o); + + o.assertReceivedOnNext(Collections.emptyList()); + assertTrue(o.getOnErrorEvents().isEmpty()); + assertTrue(o.getOnCompletedEvents().isEmpty()); + } + @Test(timeout = 10000) + public void testOnCompletedDoesntWaitIfNoEvents2() { + final PublishSubject ps = PublishSubject.create(); + TestSubscriber o = new TestSubscriber() { + @Override + public void onStart() { + request(0); // make sure it doesn't start in unlimited mode + } + @Override + public void onNext(Integer t) { + super.onNext(t); + ps.onCompleted(); // as if an async completion arrived while in the loop + } + }; + ps.onBackpressureBlock(2).unsafeSubscribe(o); + ps.onNext(1); + o.requestMore(1); + + o.assertNoErrors(); + o.assertTerminalEvent(); + o.assertReceivedOnNext(Arrays.asList(1)); + } + @Test(timeout = 10000) + public void testOnCompletedDoesntWaitIfNoEvents3() { + final PublishSubject ps = PublishSubject.create(); + TestSubscriber o = new TestSubscriber() { + boolean once = true; + @Override + public void onStart() { + request(0); // make sure it doesn't start in unlimited mode + } + @Override + public void onNext(Integer t) { + super.onNext(t); + if (once) { + once = false; + ps.onNext(2); + ps.onCompleted(); // as if an async completion arrived while in the loop + requestMore(1); + } + } + }; + ps.onBackpressureBlock(3).unsafeSubscribe(o); + ps.onNext(1); + o.requestMore(1); + + o.assertNoErrors(); + o.assertTerminalEvent(); + o.assertReceivedOnNext(Arrays.asList(1, 2)); + } } From f6fd5deffde08b35a6b01a3ac3c6f58066b3a1ee Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 11 Dec 2014 23:59:00 +0100 Subject: [PATCH 017/857] Factored out the backpressure management into an experimental class and reimplemented Buffer and Block strategies with it. --- .../OperatorOnBackpressureBlock.java | 132 ++-------- .../OperatorOnBackpressureBuffer.java | 188 +++++++------- .../util/BackpressureDrainManager.java | 238 ++++++++++++++++++ 3 files changed, 351 insertions(+), 207 deletions(-) create mode 100644 src/main/java/rx/internal/util/BackpressureDrainManager.java diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java index e106bac70d..71a5fc4993 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java @@ -20,8 +20,8 @@ import java.util.concurrent.BlockingQueue; import rx.Observable.Operator; -import rx.Producer; import rx.Subscriber; +import rx.internal.util.BackpressureDrainManager; /** * Operator that blocks the producer thread in case a backpressure is needed. @@ -38,45 +38,25 @@ public Subscriber call(Subscriber child) { return s; } - static final class BlockingSubscriber extends Subscriber { + static final class BlockingSubscriber extends Subscriber implements BackpressureDrainManager.BackpressureQueueCallback { final NotificationLite nl = NotificationLite.instance(); final BlockingQueue queue; final Subscriber child; - /** Guarded by this. */ - long requestedCount; - /** Guarded by this. */ - boolean emitting; - volatile boolean terminated; - /** Set before terminated, read after terminated. */ - Throwable exception; + final BackpressureDrainManager manager; public BlockingSubscriber(int max, Subscriber child) { this.queue = new ArrayBlockingQueue(max); this.child = child; + this.manager = new BackpressureDrainManager(this); } void init() { child.add(this); - child.setProducer(new Producer() { - @Override - public void request(long n) { - if (n == 0) { - return; - } - synchronized (BlockingSubscriber.this) { - if (n == Long.MAX_VALUE || requestedCount == Long.MAX_VALUE) { - requestedCount = Long.MAX_VALUE; - } else { - requestedCount += n; - } - } - drain(); - } - }); + child.setProducer(manager); } @Override public void onNext(T t) { try { queue.put(nl.next(t)); - drain(); + manager.drain(); } catch (InterruptedException ex) { if (!isUnsubscribed()) { onError(ex); @@ -85,91 +65,31 @@ public void onNext(T t) { } @Override public void onError(Throwable e) { - if (!terminated) { - exception = e; - terminated = true; - drain(); - } + manager.terminateAndDrain(e); } @Override public void onCompleted() { - terminated = true; - drain(); + manager.terminateAndDrain(); } - void drain() { - long n; - boolean term; - synchronized (this) { - if (emitting) { - return; - } - emitting = true; - n = requestedCount; - term = terminated; - } - boolean skipFinal = false; - try { - Subscriber child = this.child; - BlockingQueue queue = this.queue; - while (true) { - int emitted = 0; - while (n > 0 || term) { - Object o; - if (term) { - o = queue.peek(); - if (o == null) { - Throwable e = exception; - if (e != null) { - child.onError(e); - } else { - child.onCompleted(); - } - skipFinal = true; - return; - } - if (n == 0) { - break; - } - } - o = queue.poll(); - if (o == null) { - break; - } else { - child.onNext(nl.getValue(o)); - n--; - emitted++; - } - } - synchronized (this) { - term = terminated; - boolean more = queue.peek() != null; - // if no backpressure below - if (requestedCount == Long.MAX_VALUE) { - // no new data arrived since the last poll - if (!more && !term) { - skipFinal = true; - emitting = false; - return; - } - n = Long.MAX_VALUE; - } else { - requestedCount -= emitted; - n = requestedCount; - if ((n == 0 || !more) && (!term || more)) { - skipFinal = true; - emitting = false; - return; - } - } - } - } - } finally { - if (!skipFinal) { - synchronized (this) { - emitting = false; - } - } + @Override + public boolean accept(Object value) { + return nl.accept(child, value); + } + @Override + public void complete(Throwable exception) { + if (exception != null) { + child.onError(exception); + } else { + child.onCompleted(); } } + @Override + public Object peek() { + return queue.peek(); + } + @Override + public Object poll() { + return queue.poll(); + } } } diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index 2ddb582f9e..1517736dbd 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -15,7 +15,6 @@ */ package rx.internal.operators; -import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; @@ -25,11 +24,10 @@ import rx.Subscriber; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; +import rx.internal.util.BackpressureDrainManager; public class OperatorOnBackpressureBuffer implements Operator { - private final NotificationLite on = NotificationLite.instance(); - private final Long capacity; private final Action0 onOverflow; @@ -52,122 +50,110 @@ public OperatorOnBackpressureBuffer(long capacity, Action0 onOverflow) { @Override public Subscriber call(final Subscriber child) { - // TODO get a different queue implementation - final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); - final AtomicLong capacity = (this.capacity == null) ? null : new AtomicLong(this.capacity); - final AtomicLong wip = new AtomicLong(); - final AtomicLong requested = new AtomicLong(); - - child.setProducer(new Producer() { - - @Override - public void request(long n) { - if (requested.getAndAdd(n) == 0) { - pollQueue(wip, requested, capacity, queue, child); - } - } - }); // don't pass through subscriber as we are async and doing queue draining // a parent being unsubscribed should not affect the children - Subscriber parent = new Subscriber() { + BufferSubscriber parent = new BufferSubscriber(child, capacity, onOverflow); - private AtomicBoolean saturated = new AtomicBoolean(false); + // if child unsubscribes it should unsubscribe the parent, but not the other way around + child.add(parent); + child.setProducer(parent.manager()); - @Override - public void onStart() { - request(Long.MAX_VALUE); - } + return parent; + } + 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); + private final BackpressureDrainManager manager; + private final NotificationLite on = NotificationLite.instance(); + private final Action0 onOverflow; + + public BufferSubscriber(final Subscriber child, Long capacity, Action0 onOverflow) { + this.child = child; + this.baseCapacity = capacity; + this.capacity = capacity != null ? new AtomicLong(capacity) : null; + this.onOverflow = onOverflow; + this.manager = new BackpressureDrainManager(this); + } + @Override + public void onStart() { + request(Long.MAX_VALUE); + } - @Override - public void onCompleted() { - if (!saturated.get()) { - queue.offer(on.completed()); - pollQueue(wip, requested, capacity, queue, child); - } + @Override + public void onCompleted() { + if (!saturated.get()) { + manager.terminateAndDrain(); } + } - @Override - public void onError(Throwable e) { - if (!saturated.get()) { - queue.offer(on.error(e)); - pollQueue(wip, requested, capacity, queue, child); - } + @Override + public void onError(Throwable e) { + if (!saturated.get()) { + manager.terminateAndDrain(e); } + } - @Override - public void onNext(T t) { - if (!assertCapacity()) { - return; - } - queue.offer(on.next(t)); - pollQueue(wip, requested, capacity, queue, child); + @Override + public void onNext(T t) { + if (!assertCapacity()) { + return; } + queue.offer(on.next(t)); + manager.drain(); + } - private boolean assertCapacity() { - if (capacity == null) { - return true; - } - - long currCapacity; - do { - currCapacity = capacity.get(); - if (currCapacity <= 0) { - if (saturated.compareAndSet(false, true)) { - unsubscribe(); - child.onError(new MissingBackpressureException( - "Overflowed buffer of " - + OperatorOnBackpressureBuffer.this.capacity)); - if (onOverflow != null) { - onOverflow.call(); - } - } - return false; - } - // ensure no other thread stole our slot, or retry - } while (!capacity.compareAndSet(currCapacity, currCapacity - 1)); - return true; + @Override + public boolean accept(Object value) { + return on.accept(child, value); + } + @Override + public void complete(Throwable exception) { + if (exception != null) { + child.onError(exception); + } else { + child.onCompleted(); } - }; - - // if child unsubscribes it should unsubscribe the parent, but not the other way around - child.add(parent); + } + @Override + public Object peek() { + return queue.peek(); + } + @Override + public Object poll() { + return queue.poll(); + } - return parent; - } + private boolean assertCapacity() { + if (capacity == null) { + return true; + } - private void pollQueue(AtomicLong wip, AtomicLong requested, AtomicLong capacity, Queue queue, Subscriber child) { - // TODO can we do this without putting everything in the queue first so we can fast-path the case when we don't need to queue? - if (requested.get() > 0) { - // only one draining at a time - try { - /* - * This needs to protect against concurrent execution because `request` and `on*` events can come concurrently. - */ - if (wip.getAndIncrement() == 0) { - while (true) { - if (requested.getAndDecrement() != 0) { - Object o = queue.poll(); - if (o == null) { - // nothing in queue - requested.incrementAndGet(); - return; - } - if (capacity != null) { // it's bounded - capacity.incrementAndGet(); - } - on.accept(child, o); - } else { - // we hit the end ... so increment back to 0 again - requested.incrementAndGet(); - return; + long currCapacity; + do { + currCapacity = capacity.get(); + if (currCapacity <= 0) { + if (saturated.compareAndSet(false, true)) { + unsubscribe(); + child.onError(new MissingBackpressureException( + "Overflowed buffer of " + + baseCapacity)); + if (onOverflow != null) { + onOverflow.call(); } } + return false; } - - } finally { - wip.decrementAndGet(); - } + // ensure no other thread stole our slot, or retry + } while (!capacity.compareAndSet(currCapacity, currCapacity - 1)); + return true; + } + protected Producer manager() { + return manager; } } } diff --git a/src/main/java/rx/internal/util/BackpressureDrainManager.java b/src/main/java/rx/internal/util/BackpressureDrainManager.java new file mode 100644 index 0000000000..78a4d29c3e --- /dev/null +++ b/src/main/java/rx/internal/util/BackpressureDrainManager.java @@ -0,0 +1,238 @@ +/* + * Copyright 2011 David Karnok + * + * 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.concurrent.atomic.AtomicLongFieldUpdater; + +import rx.Producer; +import rx.annotations.Experimental; + +/** + * Manages the producer-backpressure-consumer interplay by + * matching up available elements with requested elements and/or + * terminal events. + */ +@Experimental +public final class BackpressureDrainManager implements Producer { + /** + * Interface representing the minimal callbacks required + * to operate the drain part of a backpressure system. + */ + public interface BackpressureQueueCallback { + /** + * Override this method to peek for the next element, + * null meaning no next element available now. + *

It will be called plain and while holding this object's monitor. + * @return the next element or null if no next element available + */ + Object peek(); + /** + * Override this method to poll (consume) the next element, + * null meaning no next element available now. + * @return the next element or null if no next element available + */ + Object poll(); + /** + * Override this method to deliver an element to downstream. + * The logic ensures that this happens only in the right conditions. + * @param value the value to deliver, not null + * @return true indicates that one should terminate the emission loop unconditionally + * and not deliver any further elements or terminal events. + */ + boolean accept(Object value); + /** + * Override this method to deliver a normal or exceptional + * terminal event. + * @param exception if not null, contains the terminal exception + */ + void complete(Throwable exception); + } + + /** The request counter, updated via REQUESTED_COUNTER. */ + protected volatile long requestedCount; + /** Atomically updates the the requestedCount field. */ + protected static final AtomicLongFieldUpdater REQUESTED_COUNT + = AtomicLongFieldUpdater.newUpdater(BackpressureDrainManager.class, "requestedCount"); + /** Indicates if one is in emitting phase, guarded by this. */ + protected boolean emitting; + /** Indicates a terminal state. */ + protected volatile boolean terminated; + /** Indicates an error state, barrier is provided via terminated. */ + protected Throwable exception; + /** The callbacks to manage the drain. */ + protected final BackpressureQueueCallback actual; + /** + * Constructs a backpressure drain manager with 0 requesedCount, + * no terminal event and not emitting. + * @param actual he queue callback to check for new element availability + */ + public BackpressureDrainManager(BackpressureQueueCallback actual) { + this.actual = actual; + } + /** + * Checks if a terminal state has been reached. + * @return true if a terminal state has been reached + */ + public final boolean isTerminated() { + return terminated; + } + /** + * Move into a terminal state. + * Call drain() anytime after. + */ + public final void terminate() { + terminated = true; + } + /** + * Move into a terminal state with an exception. + * Call drain() anytime after. + *

Serialized access is expected with respect to + * element emission. + * @param error the exception to deliver + */ + public final void terminate(Throwable error) { + if (!terminated) { + exception = error; + terminated = true; + } + } + /** + * Move into a terminal state and drain. + */ + public final void terminateAndDrain() { + terminated = true; + drain(); + } + /** + * Move into a terminal state with an exception and drain. + *

Serialized access is expected with respect to + * element emission. + * @param error the exception to deliver + */ + public final void terminateAndDrain(Throwable error) { + if (!terminated) { + exception = error; + terminated = true; + drain(); + } + } + @Override + public final void request(long n) { + if (n == 0) { + return; + } + boolean mayDrain; + long r; + long u; + do { + r = requestedCount; + mayDrain = r == 0; + if (r == Long.MAX_VALUE) { + mayDrain = true; + break; + } + if (n == Long.MAX_VALUE) { + u = n; + mayDrain = true; + } else { + u = r + n; + } + } while (!REQUESTED_COUNT.compareAndSet(this, r, u)); + // since we implement producer, we have to call drain + // on a 0-n request transition + if (mayDrain) { + drain(); + } + } + /** + * Try to drain the "queued" elements and terminal events + * by considering the available and requested event counts. + */ + public final void drain() { + long n; + boolean term; + synchronized (this) { + if (emitting) { + return; + } + emitting = true; + term = terminated; + } + n = requestedCount; + boolean skipFinal = false; + try { + BackpressureQueueCallback a = actual; + while (true) { + int emitted = 0; + while (n > 0 || term) { + Object o; + if (term) { + o = a.peek(); + if (o == null) { + skipFinal = true; + Throwable e = exception; + a.complete(e); + return; + } + if (n == 0) { + break; + } + } + o = a.poll(); + if (o == null) { + break; + } else { + if (a.accept(o)) { + skipFinal = true; + return; + } + n--; + emitted++; + } + } + synchronized (this) { + term = terminated; + boolean more = a.peek() != null; + // if no backpressure below + if (requestedCount == Long.MAX_VALUE) { + // no new data arrived since the last poll + if (!more && !term) { + skipFinal = true; + emitting = false; + return; + } + n = Long.MAX_VALUE; + } else { + n = REQUESTED_COUNT.addAndGet(this, -emitted); + if ((n == 0 || !more) && (!term || more)) { + skipFinal = true; + emitting = false; + return; + } + } + } + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + + } +} From 29c1b6e8a2645d1444f050b92e679e974498e3a9 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 10:34:39 +0800 Subject: [PATCH 018/857] Fix 'request(0)' issue in Scan --- .../rx/internal/operators/OperatorScan.java | 16 +++--- .../internal/operators/OperatorScanTest.java | 56 +++++++++++++++++-- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index 2621ec452d..6809af0db3 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -147,19 +147,17 @@ public void request(long n) { if (once.compareAndSet(false, true)) { if (initialValue == NO_INITIAL_VALUE || n == Long.MAX_VALUE) { producer.request(n); + } else if (n == 1) { + excessive.set(true); + producer.request(1); // request at least 1 } else { - if (n == Long.MAX_VALUE) { - producer.request(Long.MAX_VALUE); - } else if (n == 1) { - excessive.set(true); - producer.request(1); // request at least 1 - } else { - producer.request(n - 1); - } + // n != Long.MAX_VALUE && n != 1 + producer.request(n - 1); } } else { // pass-thru after first time - if (excessive.compareAndSet(true, false) && n != Long.MAX_VALUE) { + if (n > 1 // avoid to request 0 + && excessive.compareAndSet(true, false) && n != Long.MAX_VALUE) { producer.request(n - 1); } else { producer.request(n); diff --git a/src/test/java/rx/internal/operators/OperatorScanTest.java b/src/test/java/rx/internal/operators/OperatorScanTest.java index e3ed546347..5d2aa0bf1e 100644 --- a/src/test/java/rx/internal/operators/OperatorScanTest.java +++ b/src/test/java/rx/internal/operators/OperatorScanTest.java @@ -20,15 +20,14 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; -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 static org.mockito.Mockito.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; @@ -36,6 +35,7 @@ import rx.Observable; import rx.Observer; +import rx.Producer; import rx.Subscriber; import rx.functions.Action2; import rx.functions.Func0; @@ -312,4 +312,52 @@ public Integer call(Integer t1, Integer t2) { subscriber.assertTerminalEvent(); subscriber.assertNoErrors(); } + + @Test + public void testScanShouldNotRequestZero() { + final AtomicReference producer = new AtomicReference(); + Observable o = Observable.create(new Observable.OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + Producer p = spy(new Producer() { + + private AtomicBoolean requested = new AtomicBoolean(false); + + @Override + public void request(long n) { + if (requested.compareAndSet(false, true)) { + subscriber.onNext(1); + } else { + subscriber.onCompleted(); + } + } + }); + producer.set(p); + subscriber.setProducer(p); + } + }).scan(100, new Func2() { + + @Override + public Integer call(Integer t1, Integer t2) { + return t1 + t2; + } + + }); + + o.subscribe(new TestSubscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onNext(Integer integer) { + request(1); + } + }); + + verify(producer.get(), never()).request(0); + verify(producer.get(), times(2)).request(1); + } } From 237a728984f3a4e8be4534bf12de1cadb0026df6 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 11 Dec 2014 21:20:06 -0800 Subject: [PATCH 019/857] doOnRequest Similar to doOnSubscribe, doOnNext, etc --- src/main/java/rx/Observable.java | 16 ++++ .../operators/OperatorDoOnRequest.java | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/main/java/rx/internal/operators/OperatorDoOnRequest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 126196c516..b6c97519d1 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4159,6 +4159,22 @@ public final void onNext(T args) { return lift(new OperatorDoOnEach(observer)); } + + /** + * Modifies the source {@code Observable} so that it invokes the given action when it receives a request for more items. + *

+ *
Scheduler:
+ *
{@code doOnRequest} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param onRequest + * the action that gets called when an observer requests items from this {@code Observable} + * @return the source {@code Observable} modified so as to call this Action when appropriate + */ + @Beta + public final Observable doOnRequest(final Action1 onRequest) { + return lift(new OperatorDoOnRequest(onRequest)); + } /** * Modifies the source {@code Observable} so that it invokes the given action when it is subscribed from diff --git a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java new file mode 100644 index 0000000000..f31484966d --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java @@ -0,0 +1,81 @@ +/** + * 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.Operator; +import rx.Producer; +import rx.Subscriber; +import rx.functions.Action1; + +/** + * This operator modifies an {@link rx.Observable} so a given action is invoked when the {@link rx.Observable.Producer} receives a request. + * + * @param + * The type of the elements in the {@link rx.Observable} that this operator modifies + */ +public class OperatorDoOnRequest implements Operator { + + private final Action1 request; + + public OperatorDoOnRequest(Action1 request) { + this.request = request; + } + + @Override + public Subscriber call(final Subscriber child) { + + final ParentSubscriber parent = new ParentSubscriber(child); + + child.setProducer(new Producer() { + + @Override + public void request(long n) { + request.call(n); + parent.requestMore(n); + } + + }); + + return parent; + } + + private final class ParentSubscriber extends Subscriber { + private final Subscriber child; + + private ParentSubscriber(Subscriber child) { + this.child = child; + } + + private void requestMore(long n) { + request(n); + } + + @Override + public void onCompleted() { + child.onCompleted(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + child.onNext(t); + } + } +} From 35f2807353be5bdd3bbf35c700a726ae0d2662bf Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 12:06:35 +0800 Subject: [PATCH 020/857] SerialSubscription and MultipleAssignmentSubscription should use Subscriptions.empty() --- .../rx/subscriptions/MultipleAssignmentSubscription.java | 5 ++--- src/main/java/rx/subscriptions/SerialSubscription.java | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java b/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java index e554e0aebe..8591b062d7 100644 --- a/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java +++ b/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java @@ -25,9 +25,8 @@ * if unsubscribed. */ public final class MultipleAssignmentSubscription implements Subscription { - /** The shared empty state. */ - static final State EMPTY_STATE = new State(false, Subscriptions.unsubscribed()); - volatile State state = EMPTY_STATE; + + volatile State state = new State(false, Subscriptions.empty()); static final AtomicReferenceFieldUpdater STATE_UPDATER = AtomicReferenceFieldUpdater.newUpdater(MultipleAssignmentSubscription.class, State.class, "state"); diff --git a/src/main/java/rx/subscriptions/SerialSubscription.java b/src/main/java/rx/subscriptions/SerialSubscription.java index 4f9acdcdc7..6cc5019092 100644 --- a/src/main/java/rx/subscriptions/SerialSubscription.java +++ b/src/main/java/rx/subscriptions/SerialSubscription.java @@ -24,8 +24,7 @@ * the previous underlying subscription to be unsubscribed. */ public final class SerialSubscription implements Subscription { - static final State EMPTY_STATE = new State(false, Subscriptions.unsubscribed()); - volatile State state = EMPTY_STATE; + volatile State state = new State(false, Subscriptions.empty()); static final AtomicReferenceFieldUpdater STATE_UPDATER = AtomicReferenceFieldUpdater.newUpdater(SerialSubscription.class, State.class, "state"); From 162c31cdfec498db1f99e52a2317bf121eff2d27 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 11 Dec 2014 23:21:10 -0800 Subject: [PATCH 021/857] Remove Request Batching in Merge Removing the batching until we can find a correct way to do it. The performance impact of this change is seen here: Benchmark (size) Mode Samples 1.x No Request Batching r.o.OperatorMergePerf.merge1SyncStreamOfN 1 thrpt 5 4585554.607 4666745.314 102% r.o.OperatorMergePerf.merge1SyncStreamOfN 1000 thrpt 5 51273.033 39922.246 78% r.o.OperatorMergePerf.merge1SyncStreamOfN 1000000 thrpt 5 47.515 37.634 79% r.o.OperatorMergePerf.mergeNAsyncStreamsOfN 1 thrpt 5 90901.735 93454.726 103% r.o.OperatorMergePerf.mergeNAsyncStreamsOfN 1000 thrpt 5 5.407 4.910 91% r.o.OperatorMergePerf.mergeNSyncStreamsOf1 1 thrpt 5 4181618.767 4173322.551 100% r.o.OperatorMergePerf.mergeNSyncStreamsOf1 100 thrpt 5 422193.599 408972.130 97% r.o.OperatorMergePerf.mergeNSyncStreamsOf1 1000 thrpt 5 36886.812 36448.978 99% r.o.OperatorMergePerf.mergeNSyncStreamsOfN 1 thrpt 5 4815945.720 4887943.643 101% r.o.OperatorMergePerf.mergeNSyncStreamsOfN 1000 thrpt 5 43.926 39.027 89% r.o.OperatorMergePerf.mergeTwoAsyncStreamsOfN 1 thrpt 5 72578.046 70412.656 97% r.o.OperatorMergePerf.mergeTwoAsyncStreamsOfN 1000 thrpt 5 3260.024 3064.403 94% r.o.OperatorMergePerf.oneStreamOfNthatMergesIn1 1 thrpt 5 4678858.201 4808504.588 103% r.o.OperatorMergePerf.oneStreamOfNthatMergesIn1 1000 thrpt 5 34407.547 36364.476 106% r.o.OperatorMergePerf.oneStreamOfNthatMergesIn1 1000000 thrpt 5 31.312 32.261 103% --- .../rx/internal/operators/OperatorMerge.java | 45 ++------- .../internal/operators/OperatorMergeTest.java | 92 ++++++++++++++++++- 2 files changed, 97 insertions(+), 40 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 6584f4fac6..297a4412f9 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -186,8 +186,9 @@ private void handleNewSource(Observable t) { InnerSubscriber i = new InnerSubscriber(this, producerIfNeeded); i.sindex = childrenSubscribers.add(i); t.unsafeSubscribe(i); - if (!isUnsubscribed()) + if (!isUnsubscribed()) { request(1); + } } private void handleScalarSynchronousObservable(ScalarSynchronousObservable t) { @@ -382,19 +383,8 @@ private int drainScalarValueQueue() { public Boolean call(InnerSubscriber s) { if (s.q != null) { long r = mergeProducer.requested; - int emitted = 0; - emitted += s.drainQueue(); + int emitted = s.drainQueue(); if (emitted > 0) { - /* - * `s.emitted` is not volatile (because of performance impact of making it so shown by JMH tests) - * but `emitted` can ONLY be touched by the thread holding the `emitLock` which we're currently inside. - * - * Entering and leaving the emitLock flushes all values so this is visible to us. - */ - emitted += s.emitted; - // TODO we may want to store this in s.emitted and only request if above batch - // reset this since we have requested them all - s.emitted = 0; s.requestMore(emitted); } if (emitted == r) { @@ -542,9 +532,6 @@ private static final class InnerSubscriber extends Subscriber { static final AtomicIntegerFieldUpdater ONCE_TERMINATED = AtomicIntegerFieldUpdater.newUpdater(InnerSubscriber.class, "terminated"); private final RxRingBuffer q = RxRingBuffer.getSpmcInstance(); - /* protected by emitLock */ - int emitted = 0; - final int THRESHOLD = (int) (q.capacity() * 0.7); public InnerSubscriber(MergeSubscriber parent, MergeProducer producer) { this.parentSubscriber = parent; @@ -618,6 +605,7 @@ private void emit(T t, boolean complete) { * putting in the queue, it attempts to get the lock. We are optimizing for the non-contended case. */ if (parentSubscriber.getEmitLock()) { + long emitted = 0; enqueue = false; try { // drain the queue if there is anything in it before emitting the current value @@ -660,30 +648,9 @@ private void emit(T t, boolean complete) { } finally { drain = parentSubscriber.releaseEmitLock(); } - if (emitted > THRESHOLD) { - // this is for batching requests when we're in a use case that isn't queueing, always fast-pathing the onNext - /** - *
 {@code
-                     * Without this batching:
-                     * 
-                     * Benchmark                                          (size)   Mode   Samples        Score  Score error    Units
-                     * r.o.OperatorMergePerf.merge1SyncStreamOfN               1  thrpt         5  5060743.715   100445.513    ops/s
-                     * r.o.OperatorMergePerf.merge1SyncStreamOfN            1000  thrpt         5    36606.582     1610.582    ops/s
-                     * r.o.OperatorMergePerf.merge1SyncStreamOfN         1000000  thrpt         5       38.476        0.973    ops/s
-                     * 
-                     * With this batching:
-                     * 
-                     * Benchmark                                          (size)   Mode   Samples        Score  Score error    Units
-                     * r.o.OperatorMergePerf.merge1SyncStreamOfN               1  thrpt         5  5367945.738   262740.137    ops/s
-                     * r.o.OperatorMergePerf.merge1SyncStreamOfN            1000  thrpt         5    62703.930     8496.036    ops/s
-                     * r.o.OperatorMergePerf.merge1SyncStreamOfN         1000000  thrpt         5       72.711        3.746    ops/s
-                     *} 
- */ + // request upstream what we just emitted + if(emitted > 0) { request(emitted); - // we are modifying this outside of the emit lock ... but this can be considered a "lazySet" - // and it will be flushed before anything else touches it because the emitLock will be obtained - // before any other usage of it - emitted = 0; } } if (enqueue) { diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index bfea937f75..48e7680581 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,9 +41,14 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import rx.*; +import rx.Notification; +import rx.Observable; import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Scheduler; import rx.Scheduler.Worker; +import rx.Subscriber; +import rx.Subscription; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; @@ -1105,4 +1111,88 @@ public void shouldNotReceivedDelayedErrorWhileThereAreStillNormalEmissionsInTheQ subscriber.assertReceivedOnNext(asList(1, 2, 3, 4)); assertEquals(asList(exception), subscriber.getOnErrorEvents()); } + + @Test + public void testMergeKeepsRequesting() throws InterruptedException { + //for (int i = 0; i < 5000; i++) { + //System.out.println(i + "......................................................................."); + final CountDownLatch latch = new CountDownLatch(1); + final ConcurrentLinkedQueue messages = new ConcurrentLinkedQueue(); + + Observable.range(1, 2) + // produce many integers per second + .flatMap(new Func1>() { + @Override + public Observable call(final Integer number) { + return Observable.range(1, Integer.MAX_VALUE) + .doOnRequest(new Action1() { + + @Override + public void call(Long n) { + messages.add(">>>>>>>> A requested[" + number + "]: " + n); + } + + }) + // pause a bit + .doOnNext(pauseForMs(3)) + // buffer on backpressure + .onBackpressureBuffer() + // do in parallel + .subscribeOn(Schedulers.computation()) + .doOnRequest(new Action1() { + + @Override + public void call(Long n) { + messages.add(">>>>>>>> B requested[" + number + "]: " + n); + } + + }); + } + + }) + // take a number bigger than 2* RxRingBuffer.SIZE (used by OperatorMerge) + .take(RxRingBuffer.SIZE * 2 + 1) + // log count + .doOnNext(printCount()) + // release latch + .doOnCompleted(new Action0() { + @Override + public void call() { + latch.countDown(); + } + }).subscribe(); + boolean a = latch.await(2, TimeUnit.SECONDS); + if (!a) { + for (String s : messages) { + System.out.println("DEBUG => " + s); + } + } + assertTrue(a); + //} + } + + private static Action1 printCount() { + return new Action1() { + long count; + + @Override + public void call(Integer t1) { + count++; + System.out.println("count=" + count); + } + }; + } + + private static Action1 pauseForMs(final long time) { + return new Action1() { + @Override + public void call(Integer s) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + } } From 0c135fdabfde4a0b70c1ddd238f64263344215b0 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 22:39:26 +0800 Subject: [PATCH 022/857] Handle 0 or negative request in Buffer --- .../java/rx/internal/operators/OperatorBufferWithSize.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java index 6f14f4bbf7..60872b5ba7 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java @@ -144,6 +144,12 @@ private void requestInfinite() { @Override public void request(long n) { + if (n == 0) { + return; + } + if (n < 0) { + throw new IllegalArgumentException("request a negative number: " + n); + } if (infinite) { return; } From d5b4d6f380a02626fd4e9d66a27851b9a70aa403 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 14:41:49 +0800 Subject: [PATCH 023/857] Fix NPE when the key is null in GroupBy --- .../internal/operators/OperatorGroupBy.java | 19 ++++++++---- .../operators/OperatorGroupByTest.java | 30 ++++++++++++++++++- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index f934856164..39cfae4457 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -102,7 +102,7 @@ public Observer getObserver() { } - private final ConcurrentHashMap> groups = new ConcurrentHashMap>(); + private final ConcurrentHashMap> groups = new ConcurrentHashMap>(); private static final NotificationLite nl = NotificationLite.instance(); @@ -166,10 +166,18 @@ void requestFromGroupedObservable(long n, GroupState group) { } } + private Object groupedKey(K key) { + return key == null ? NULL_KEY : key; + } + + private K getKey(Object groupedKey) { + return groupedKey == NULL_KEY ? null : (K) groupedKey; + } + @Override public void onNext(T t) { try { - final K key = keySelector.call(t); + final Object key = groupedKey(keySelector.call(t)); GroupState group = groups.get(key); if (group == null) { // this group doesn't exist @@ -185,10 +193,10 @@ public void onNext(T t) { } } - private GroupState createNewGroup(final K key) { + private GroupState createNewGroup(final Object key) { final GroupState groupState = new GroupState(); - GroupedObservable go = GroupedObservable.create(key, new OnSubscribe() { + GroupedObservable go = GroupedObservable.create(getKey(key), new OnSubscribe() { @Override public void call(final Subscriber o) { @@ -252,7 +260,7 @@ public void onNext(T t) { return groupState; } - private void cleanupGroup(K key) { + private void cleanupGroup(Object key) { GroupState removed; removed = groups.remove(key); if (removed != null) { @@ -357,4 +365,5 @@ public Object call(Object t) { } }; + private static final Object NULL_KEY = new Object(); } diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index 432266ea32..9bbed5d04d 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -1357,4 +1358,31 @@ public Observable call(GroupedObservable t) { }; -} \ No newline at end of file + @Test + public void testGroupByWithNullKey() { + final String[] key = new String[]{"uninitialized"}; + final List values = new ArrayList(); + Observable.just("a", "b", "c").groupBy(new Func1() { + + @Override + public String call(String value) { + return null; + } + }).subscribe(new Action1>() { + + @Override + public void call(GroupedObservable groupedObservable) { + key[0] = groupedObservable.getKey(); + groupedObservable.subscribe(new Action1() { + + @Override + public void call(String s) { + values.add(s); + } + }); + } + }); + assertEquals(null, key[0]); + assertEquals(Arrays.asList("a", "b", "c"), values); + } +} From 522ce798debcb083156457a18b15faad01474749 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 11:18:01 +0800 Subject: [PATCH 024/857] Fix the issue that Sample doesn't call 'unsubscribe' --- .../operators/OperatorSampleWithTime.java | 1 + .../operators/OperatorSampleTest.java | 21 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java index ea94a7db21..7138d760d4 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java @@ -50,6 +50,7 @@ public Subscriber call(Subscriber child) { child.add(worker); SamplerSubscriber sampler = new SamplerSubscriber(s); + child.add(sampler); worker.schedulePeriodically(sampler, time, time, unit); return sampler; diff --git a/src/test/java/rx/internal/operators/OperatorSampleTest.java b/src/test/java/rx/internal/operators/OperatorSampleTest.java index 0a8c9da58d..815d002061 100644 --- a/src/test/java/rx/internal/operators/OperatorSampleTest.java +++ b/src/test/java/rx/internal/operators/OperatorSampleTest.java @@ -28,14 +28,12 @@ import org.junit.Test; import org.mockito.InOrder; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Scheduler; -import rx.Subscriber; import rx.functions.Action0; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; +import rx.subscriptions.Subscriptions; public class OperatorSampleTest { private TestScheduler scheduler; @@ -271,4 +269,19 @@ public void sampleWithSamplerThrows() { inOrder.verify(observer2, times(1)).onError(any(RuntimeException.class)); verify(observer, never()).onCompleted(); } + + @Test + public void testSampleUnsubscribe() { + final Subscription s = mock(Subscription.class); + Observable o = Observable.create( + new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.add(s); + } + } + ); + o.throttleLast(1, TimeUnit.MILLISECONDS).subscribe().unsubscribe(); + verify(s).unsubscribe(); + } } From 4c1417729f4810b4fe8ac974b68564aefad2dd60 Mon Sep 17 00:00:00 2001 From: David Gross Date: Fri, 12 Dec 2014 11:41:58 -0800 Subject: [PATCH 025/857] add @since section to doOnRequest() javadocs --- src/main/java/rx/Observable.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index b6c97519d1..b4a7b2e43c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4161,7 +4161,8 @@ public final void onNext(T args) { } /** - * Modifies the source {@code Observable} so that it invokes the given action when it receives a request for more items. + * Modifies the source {@code Observable} so that it invokes the given action when it receives a request for + * more items. *
*
Scheduler:
*
{@code doOnRequest} does not operate by default on a particular {@link Scheduler}.
@@ -4170,6 +4171,7 @@ public final void onNext(T args) { * @param onRequest * the action that gets called when an observer requests items from this {@code Observable} * @return the source {@code Observable} modified so as to call this Action when appropriate + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta public final Observable doOnRequest(final Action1 onRequest) { @@ -5066,7 +5068,6 @@ public final Observable onBackpressureBuffer() { * * @return the source Observable modified to buffer items up to the given capacity * @see RxJava wiki: Backpressure - * @Beta * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta @@ -5089,7 +5090,6 @@ public final Observable onBackpressureBuffer(long capacity) { * @return the source Observable modified to buffer items up to the given capacity * @see RxJava wiki: Backpressure * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) - * @Beta */ @Beta public final Observable onBackpressureBuffer(long capacity, Action0 onOverflow) { From 39c78562e4be93f44e8304062d272f18fbb86585 Mon Sep 17 00:00:00 2001 From: Justin Ryan Date: Sat, 13 Dec 2014 00:15:29 -0800 Subject: [PATCH 026/857] Bump to 2.2 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 49875 -> 51018 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a031f7e3ed..4e3b950704 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { repositories { jcenter() } - dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:1.12.+' } + dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:2.+' } } description = 'RxJava: Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a7634b071cb255e91a4572934e55b8cd8877b3e4..c97a8bdb9088d370da7e88784a7a093b971aa23a 100644 GIT binary patch delta 43634 zcmZ5{Q*b80)@_oBtuMB1+qNgRZGRKnb|$uM+n(6AJ$L@|-?~-jKJAC@uIh(XYp-6t z_F~A7iP#^AO0qw|VL?Ekp+RodJ;f6c$r1ji`p6?D2!nutXe0hYNYh*NBW8cEf@Z+X%#w2xp%>DYb019MT)K`Hgpqxfuk1^&xrLxcK+O z4m#_U@zbm#nc09n4E!=0q+SucjoU;BcDA5Yhu;L6fsA|1EUVO<_7BB+V8ObeVo!b5 zQD*HImyRZJ2gzthhQMQ#kKp};kHRnRD6SE>-;k2}@j{opyMX@*I@5;phgqq`kr2r> zEDVx>`SN?m5!<;}T>-T6MBx`PFWEbvWoMxPyp^9~q5Y3g`ci>}f+EZ&+TmVNctvxv zZ&G$3FFq*>O_f_)mCU%b*Z?($HT)YkJr29y7VQZhn$2U{&p87;X1~hE@Kno_#A3{l zkSnwyfh081)RjY~wLWp}YGp86UE(Yu#5d-48ReLxUgGZ=g>2(~h4m3?6$8gT@Z-{Q z=Nrn2BgrG^sonLqP1T0_dJGO7S^H$Bm4a>xfG**JM%WvtV@8#{K90C{u3urrf$aIkT6WH7Nc za&d`K)ltL|M*o&b&|7&@siJCbvDN|C`J;WMzBn{l5upZ61fysg0#n1#Y5LRlQ?r}Q z+InGB;8BvDe^7;XEU0t5ecGeP<#=l9Mo`cnOlzPbPb{ug8tQny#>LJy zSy!n&<09YLF~PJILu=M>7M(kdDeL2%U}U*ikDF`c12Jgryx$wL`T{4w)@Ht5@{;gd zFqeu6oFfO8Ji16O>xR=NY;zhn3CUu^pMel6HsMsMz06L7u`19h z(LJMQS9(Oi*4BBkG_km+Xa*S3ygffYmfNuL?pVyB(Wbg%u#+N1a|yF};MIDmLP zij_K1U5rUsSHP=5I{Xktn5;{sJ{kb|WR3s|l)t^MFY^on0A7&I(@%3hBo>>1G@EA7 zdW)5wQ65XRsgH1#o^eR<4<2|>ZnwQs@3-k`-LXp7ReN_hgN7+S*sH`iH_^lxy?*4V z-S*?2j6^w3@uzKmGNG2!HB5Bd5e@txPC~f{VOZCrK1(BXTkp!=_+R0XR*8UNEUST} zElD8y0p}+XaR!%YViNki6zc-;2OOrMflfTtxFIc9EEJhK;V)`FGW-EHg61SeZPQ1# zLBF(9#_O*@?+~GcGRU6bptN`%(ScXUpXsY1Bx3L+8sYBnandtHq3ss~s2Eff&*Y2e zy)|jnI=-r!&;Z8#V5stM?xkDg2p4!@7~S)L{Uy1GbBVh9Gvj|Cit+zA5^rB`V;BMi zwKkiG*cj%4V`Ua@lN`aa3J}ADgX1%?VSL2;?bn-^o1D%u^%Tsi&1X zqY8s6QZyjl)L&UyBezSnGTYL^^?kGoQ?w?Uoa?iTP2Jcz?5W?ntcTYbO&^VgaaDQQ z^z0^0mqdQcGACzSqm(H+L8Go0*yCklqdC)GS6kY61L;g~G78MfL-A#)IvDs`*ruZ1 zUw6IOO%~Jhh;`hvrm^BslI+bT$d=j@ArUM>0fS0Ufd>Ib(cyCC4|~k4a7N*C$zan< zf02k_K=)?Z8%9*3tGLf0FA_Os|6KhXLhF+qJKm%d&T?c-uGUNc8}Vt2K%k3nWW%%N zoc=Dn0u-Izz`+gno3A{AA7;>Ay0MlYO*4PG^;;fxTj*0CSXVYPR+2w87v}%Dq^6Tc z^y;&M7pf!Mk`>1#7hpWd2kjzdRZYT5%V@O4B7vn<`?HCqm}RAQ)6FNOA)4x*gqo0=0r|REDd>}8^U~v6*O^svhd(uGso8>CjWe6`48qdM$ zktDUkG<4b4RCnIiI0imL!?dQI^g3JImX$^9#i^qEO2mIO&cm!L?B(lKXe4ax<1QgO z4wzt(n?@ZHT=J3WggCgLo2Y~VV6UAhe})l%RJ5`#K!^&PRZOrqOP+d~4?QB_$-8wh zjiBjB?p7;Q6JoAzIZGtF)M^>V#6rdA;YnPs3jFas6alZuF5!>d2CLyE9If=(|mlQF+;B>S*wEM+kwrPV?O3ZHG4un-S z*-}r|b4vE$wbrlMXtS4uK+s&gB~T%$!O5({bW|9KIm`|wMVe*4EJWY(Jb(S220G6A z4BngANAM-qy$xo3oN zFGEe>3x1ll4BHa6I%8zLjq@@`1W2V%P;$;A`F=fXL{p38)2>u(dym7R+rvwG$u^zu zo)1pcl#m-TPHhVt;zdy~-Duddx#@!MhPn?hD%d>7BBL0k*xPbJu6ZRP(AB5#WYyyzVDX@G%B2i^to6~A&; z6MnoooTI=e@C1&=OS0|^1+@ih$n7KZH_vN=85H90&r}FPxd|Zt%u-wD&Y90Rw z>Jl9Pi0a}!{1VMAb4CJd@(s#;ZVZHC7Bz@V9FdvP$SwWxNXK1jKH(Yu%0WO`9nI29 z9E&m1h183>NSu7Ylyb=ZrcX)rmyB9p;aSNq|z8AU}gMV`Cpy|}3%)0Aezt9ck zbPF+Mgs3b#Zn371`9UdtMcy5k&9>3J15=dBvb?#0UA9pqiA}>ybk@fAO!(irq;UwY z6zo63MA;AD>gb;+!T!hoO#fm3$Y%VcT0}UELHEf*B^j9zXfUhy@LvF1SeiMRVx**5 zL@c1D0C&(JR^QB+$6u-^56<7yQS0X3*WD*>vu`;rvzI%6Ki@EaeZBr1WQZ>F6ct62 zV9OpVi{xZ~C4WeU1en}2F?gWSP^f2X+@*$UM1wm?)WAs3$T%wm#g-Gz*m&YDE?kPh zZTAmj=nixyc2P3&QWoFXFfoxzs|?ZwtK|Z1gkh}!C6SY`V4@{v9g+Yx$&OO=*Vv6l zk^WHTMO3tq%V@_L99C@S^^wb>gbWru)!a$Byey6fSnp9=N`Nf)h^@rXF+OP;ngU&Y zx`+64lgYzwCJAt%j>>%>mZLOShgQ9gmBsx}oB5C&FNd+YSXZYjU7I-thy9@?CL6Fi z_yU@=l1QzX*ZK(7URsPjB%qmbQj#(ZGm+#Zvqk*p1E8MD<*o>OP-fWDT$?&_scMg6 zap0%>_10y0vexcbuGEMjSGkCWmVI;Dc*Z2jnaZ|lPrjjSNRYB7SE!Bb##8%|tJ#vN znnhK9aaH|Tuo;Rq%A7Ocmt8e-EeMdBbsU?)p&7@_xq~cH_Msw>;W!t=E&qM}w|Ct+ zx8#|l-hkZCP#wop6VE5(nm?tfb#MwQ$-C#?60%RSs`#D4r<~QBE(>F&UTKlEJS>Kb zwpPJufw2}%iI0|NbqR}3>hIf|`K9QtHQ@dwhVAAWYS6sajXWAmtc?xh?npr1*sI`^ z#Q|6gTa(m=TN$@Om*}H#L?|z5U3>`LB+zNvwRD5fJ!4>z;Ci*O@F$Th)D zDlv4hI+a+?_Nf-ggnlXB!?xCyCoDZp33j&;J_wvLnTc;VUwQA0C(5AN zoWHXEgu^0oG8rU^^3CorjP+7bISV9Q?@^Da@14g91mAa3Zp-4_rf<~8sWDO$1=`nK zAd4e4TUPP?LK43J))E0OK8mu%iAUXjfdOZ~$X~~yqZx&BN=|mg91Kt{UTe341WuE( z;%~5M-NiO{JEE)buC!Fot?an(evQFyNlx+#mEWvYQWYQE8qtTQ_wFUqri zY-kC7rxsvep7T1QT*1OkN+iPRBD`maC2eT9$RiZzVd0w$GA}5x1YirZPsrX@0P*bX zheKVGiq*3_3c(h8r_HSq;mu*$HpOWuuFC)(s5he}g-L;J3`xu@0CeW*%BPOyxNQHp^IinWdi`<&YZ76~G+08y@ieck<+73D4&U~LMV)`E9V zr-^<-oQuc62Et#c|7O4$=Zi=H6bOhtA_xe@e|WD_1_AiL53m%CHxpbYM=%zeaT|uCV&=qD)59+`v*?G zV0qDds!qN*c?rW7;vM+IC#K)Nc%!!s@CM=gQOds5p^T&RA+dN1l@CKM!JIH5Q z%)YY64x_g@SUtF-FSmaEPL!d9L${*bUX+o*PFTcfttf4$F-P4EEvoiVGa4nAlJbop z@o6Vl$^Hb{q%Tp@!3S2=!K8<@QOU5A(h^D+lQeWP342S}{UrT0@(+0)d0mXEU3hY`(0aDSiV~=WHlmn@k8V1%V?# zwU?^Ex%`yU^>38HrBRtlxDNhmdUUrt#$5~cjd-^@Z09uif@{3`OOX|ilx{N{TH21B zoVm?ne~D%}`K(yPBKHmqhYz@pPFBK)a_^ShI|~V)EvjT&dDmHw-s)8}2kEv$Tc=I{ zgxmCzTNzPHKKFY%MMU(G)e2^o?W8dftED$`f>0M?0bnBD?|ELm?1SmFQtsPiipgj* zgGzopmqt)?P<*}DJ=t+!3E?+JbsAflKd9;aIGkwLX7gwM3aBdBRd)=_i(7+8rG$0x z+fE})6Z0vBNDyVzrGH@ky}?bDP~XjM@ooW+rTdacJ`i_YBG!6s6bW|Sp z;v*Q2Q-ePkrO6#+2e^1@536Dwh;Q5f99F!IjD(=Omsu^qqQV~TvaftCKmCLkw2)|^ z@h7f?do7MMh%UmJ8GMwp^?B|S&8eY&f|5+ z@_mFZX=m`9ILyW&^VKxYODcl0*D&-`uy1L5Sk@VG>F%OEGTCFe61OPBM29qEh|=vn zdGZc54|;Z_m8Le(I7j;O4)j{}+*?b06ist|;TztLUC`blz4jF~N8(QF4hMYE#%~DY z!Fy&%I6_h2R!>*s=qm7`u6v_$L$xs|F^h_Hl|Cz2^)`SfK_9>T$ni_yZtky@x&Ohq zSKKj)317F5gvV7V^BIf3DEvQ%i(Ep6)@!=B-SPbVml)1C)HX?ZcCQOnN+leJ-i=w= zwH)K06?!k5Zg7q})_~oI_w!db=ATf!4;`b_^tR7?gXOw@z4t(C@XG}QIxF$gk!|z9 z+RLiN%!Bhue^g4_D0IhFcTkq7gU@ZA35QOJru{EZ_$nGrgfT}y`;Cq?L3fP_=Ii=- z=T9QdI7bbxutfk>t#<|CKGq4mE78zRqrXf=Z7?XjGsX|mfB?!e>JBI7_?fQQ(25kl zUF+(=$H~88tAxUA69_aOb7>Z zPExNi;PQ`=g%tQyfsYUv{h>1A)&CO;N@A@Mn_Z zx{2I0(UW9)Ak)?5FjX-$``aeOcw0YEBs;D+qOLw#6o5w{iwDyiQ0NkqQQod}v&7$2 zvzmO(ChI;Tn)0mZ@SEqDScy;imR*T8zzIn`sLLW?r<0G8@&ncG<#vuLX+>aVQAc7F zGIly7{0PVoAj?urcJlRe{h{X)fmSs?4wZX!K<9oETlM6ECxph#mc)}o=Z-l^|2o6c z1HH~1L7i)9Y+k}FN~f1>aTkp#zk~5E;JYTeLu3E}x?r<}QP6CPij?MbF7lxl%zBYA z^30;L+(p)9nQI&)Vd<6JPVn+c`PHaHpzS#PfD9}iQRlmu&&neXV9Z)KE{%CRCAdO4 z&hA(f4&%Sz$NzmYDW%CzU~X!tW+YL>Qer-mMD%T~zTVQ+Vni^zD2Wx8@1?8k66B^$cxwxzbUQfO-C~MuJ1nESq@4~-k-xa zQY10!cmdfG0m<4`tst*#qS*5g{B;qYU|%KBEnXo0&4boxSW2@m`o!P=&o+PEI*7Ra z*GO}SKtPE8HB-za{ucNoW?VF&35=`krk0_t9u|2=G_q&^FUj=;tgcSd*_lAGOD-oz zM_WoOOYhw#`E#CP`z;Et7E_(O`4YFqCiz7Q{=^mqTuOd_JMSGU(Z3GnidM`cr=u@^ z5BN8qy)Qiuz4wD@2IK|wpLay!XNpB{C2Tv1$BMG^pBd5Ld{SYcEJtgna`u9uy_2oc4n{gsY8&OFpyH`um10ww0XffL7GQJ(07ZFZ*r-a z$68@+`M(qoe+lyc#(xU}-fM}fOF4toI_`HboM^(WIprKw@f;Oltk`}#2txcvy6UvHn72FE5 zR-=tW%11m-fjTX>DEydrZvF9dye$Wi6Pq>Q!iU`$zajddE)+$6>gX9?W zm=(NXZiNG|hh3oJlwDLAsotcJ?;OAv#wjJt-&6S0Ccak<>j1X^RQ`jzcjasd^>~~k zygl4`#2xLz7fwzyu9J={=_B?hmc-C=&=C$V7O(Oi}el-wk^PG>1mO*O|gH&#r# zU>Uqb9vs~i|j96 zlsgw90VQBY7zqMxF<5PTnhgK-n}p>FTSFr?yP|@0QrSeJ$4JCbC@;Z8I>f=EB6H=+ z*BDA3t>wxVNJrL)cRwkoo#l`!z|U=k%~}(=U1lWQtbPqsHOd27k7=QSDPm+#@gwXu zvD*i({c7-!(3CoM@}O*yo%YN)?~&!rGF>IXKL=3F+d)pzFaRoD1|D8!H2N&qyp+zb zojEm;kwjNdrS-$&X*`+!J^EZ@Q;K5v7^GjtIxK`7h;iQVz+YZYy|qvX$^KW5&wr3!tkIt?_1_CdEpj<6?6MuRVqHa)X( z;D-bP&7HN9l8Tl2OJsv}#OXp}WLS=h=*OZMIerC=t18VbfHqZ_468I!(hT{Q4{`sP zaG;93^tE_>47Qs#``TN>k>o`d`D!3xhOn3_vaOqIZtLR`H6?S<*W#t?R@S>uA#p{3 zxnJj-_=-@|ojt}l(xp(aNpn#Ybvr8JX61|rjvegeE>b)AD>{|yc9Fwo=TIT93ugic zK#89TF8X!lE^(LeHdZ~{-B}PYYEjR0{RYnpO(@!Do_pvk3)*>m+srNP zx&^8)juYUk?L;cCBrNf#@wXYmUN-B#?u9dnn-4;d;s;I4Co-4x^2^Wst{UWv^YZh6 zYG-hdT{695vlWlZ@98p-IT~k!uEOL=Hz1w39Y4o@+o$X&E1d~MQg0SIGrmx7GJg4| z+#LR(xo5D~u&J#U=l>-va?VfFn+8Sf-lGAFRL&=foI?~S}Aj!1o5Cs45Qk4$L30=4M5jK~&3F_mwG?>jXRsFmgs%!{VRRh`g)UfYc} zqlhVS4=}%-RgQdqs(OkXNfW4YRV<_xjV&?o=N01-EZSM7P-pFB=x)I)W@u@CAHch5cUb%ThAji#um`mN-WrkXVS|H1?VQS*R$`&lQ4nkm4X4N_w$q z0i;T$(En-qzN#csb*asW6vxj8lGhf}T(=f7>&b#qnQJkO28N@P7%|w&g`7;Oz0&F9 z28N(Nua!PJ;%FZH`1RYEVgrq;jE3@$23PStwm{`QU11Hi#nyV|J-2_}1k!B~O|OBH zl4{d_A5S7G#v5^nVE#Q61UJ${7z>Ai27$T&Sg&osh8;G67z0#Z~^ zyt97cD!^auDDD;?Rah@a1F=9ptu~ zFl|!+)^csJgV&aLtqtNSFrI-@6@$UwU6PSY3d@YfUH>~I`6xsk55YO|%PM;jzin52FQDA?LP#0q>%9tYQN?RHykti9x78os z;wh-kgEZ){^{@oq2_(I*K{6qePE!_<}PYLz~0uvAD?IR(s8>yl%8wijGY0g(=6cr_=lg&kY|@ zWZh@*WUZ;5x`|s*ZdFnch@-6Sgyv^c-93rOA_y;)*f=xb{e%uO)%tP17X~(zIL#<2 zP*Irxwjxe|m&Gln4cn2RQ99{{6a$!?ipN6VEm$&|zzs6g5G-I*DwrlBh?@s{c_yIr zb@#EE$11`NWbiR?JH##Az4D~rRl_VK$E-}JUa6*Wh7Co%l_KX^A$5PqX@;+l3Unx! zcV!hXj}7laC&?)Zx56w(-vQ2b3CTQ= zK#-&q10IhcV0(njrs)~uYule7RBE}z=$k5?eo?`PUXQJ3c@f3}fpeL}R zqO4&@Ad_R|{cb!Bd9t7q@_*dWIkp;BdUz-@Rub6cP^9hbvea~YWBQ44Pp=|g#C-_o zrGr`KoLiw^dOpjTV1c$SK?5wM7+aFFDO*mk$deVMopsMF<{sNEMC6gY`4Qrr?fXjr zq`i{(3+l}1v>6geI4~x8fzcE8$3C8~QJnBF^a0ry8KHdQvxnN9qnIKV@ z)TY73uv|n&Vq7>5{|m5=_N2)gA=#cdt`Dzu%heviC~Ka?y$8;y>ZCCqW|cluS{=A7 z)l22nEvHUYKQtkHv=nBQ;y*M!uv{4w=_QD-<0o9TGAONrU7g)CF6iU6Rm-AWTt&G; zZ0E1Xt9W(7g(w-?=3a9-zny!CPVl)Q8I&wDtm?y0ZW{e{+`w((j(hno?yA@DqI{=S z>I%&*nOr(}oCPWy@F*xK(XUgd`{Nw5l))za3`*Y&MV|{9nhPq*@7J}c`yfvLWXAfk zK(16=49e$_oFy_A2)lC$&pJV2WnBC?Q@{MgC^+GDjN)<+op9{I@4qFJ`$lG;D?Kgk zy928(%!9lEY1=}o$P10??EB05*ywo2y7qzog?F6mApun6ef6ikcy2vyjNPa_wRwEN ziU>6bkyAzdr#j?EkCc5=q(0RMJQE;u3ty%deV}w`piZpH4z2FJ@}Bszrg2gb<0Pg0 zAG8qO&O!2ulH~DVt+r|I2N`*!o5lQTq#xesiqH+Ot(^jY<8R%@TkLeKXyz4UW#QX4 z*KEK#(gWiyN?S)h$lmy7b6=$L4KJh=!ef}AiSYp7MUj?e5_oJxsF^4b)vw#I;FLK{ zM}u7=y>h)^^^+*Ld7`$Sj^=27xiwrQHxrpQn7WU3r1zMHN|bYN_Ku0rZEaG8D5lk& zy1MwuhwK#NlYP6$q)0KHT7te2*5XMTZ(Lw{$G|gt!^ev#bkM#y3D^~k(>1WI#Ulqq zFSB#cv0foc@JmX@MOe&dO3?|Mv|4|a^ljQCPz!X}E_VnVd?@rG0u0x90q9d*At0zi zdwlbXzbQOebl|UPnm#h{v86%mYLH>OUxWCL%T8QpY*nTMA{g&bhIwIzaSwIF5RZAk z#DOf>Wx3ZSn3e=B{IT~jLt3iWp0k zH{v@G)yX(hyA zy1V_rS>HzfLVg)B6Ss3+*+5795p;h3%N$Q1Q3Wpj<|8$7IRy3nCHUX&@>ZW*#L&NA zwG^2D8}~_yY{5ygr-21RRG{|RGJ`vUp=S*%sLJ5xv%!SaVI>--@)SkYF-nM~meHY| zrZQN4;J&Y?^>8qT*80ltd@#N*0q5OoOoKS@mytX&GI(8Prn*hO-ew&bLAwG7&0d`} zB^b@gx)_Z#BxsLKHwU|-|CxS9(xFhy*ppg&gJ0f!yNO{>S}8!}G$yh_6dTkPGh0K| z0jMV_N6xs{y5X?Vr&pyB$ycob$|t2aXB`R5L{t?pU)9Xte-^>`e&2?IIE~0LgT^pV z8`8ECw{?;jvb`ve}hW=ZNQNpMMK97I?@n&a5~6 z)LLfmyhBs>0_16*#s?<}X7s+Eo$iiP|?Z})Skz*g0!g5r^nu@Kd9g$J{bPQGEB>|l# zug6D0ntqy`+GeK?%OQuv*qHyh5Zl4iV9ThuIq%QxRhMa8BsxW1^wp4Ian>?q4(oy%$!7f6^0AI3dia#))k!j#QQOnoZ zBiRfJB&wnIglYwM)s5!?l_{6#M_fR42UxBp-HHXoMuP<*38$kz1AJj+g)v+yy-#t( zH!H*+!4809{1&tIRAYO;<{p;Nsv|F{JtIXy^IAME+J=88AKq|QhS6D8q48hDTrm}NkaZ2!) z$}c(K);sA=RiH8aATe*MAlVD46s(f%?084{!J&Xq4M8Xnvv>Sq&q2TX|GqVy#$^AX z6+Ud9ep07#Ri)DP-om7qq-|EJi&pZ2oA?Jw zEec!h#<3m>om9^;k~1)|qvlFmuGqqZI-{|zaKERSu8Q_(w{a@B{O*%iv=(|bf3f5C z8%hMYJY5=T7@%EGzFFgBUU@F}svdJNAEV}bVsi4EUpoi?y@WyNq50Lq;BqaBgK{)bs;PEa!HsF!G?A5GYZNkGd)WMkrrtj zJ9Msi&6)3y)ziN#NZ$`ayYw+57fn$MbaR(Le4fzTKn%Cj!ubeH=rO+C{lI zG~$N+u)h?%5y#WN6HJz>vj>upfnn*l(Z_q!gP4K=$&z2pI21!@W1!4&t6&8N;|B}D zoV!m36c;1Gr4aIWe3_KKpm{>5zyVgdkJM>QL~X>XP!Mu0mk94uPV78OdD@P*R^e<_c=Q4qLC z;FzaV#`!-_lV?@#aEH7)OyF0|0l#(x-u6$K)NHZaz#aGf%{Qd~rn-J{q6yJ|;eqCV zi889Bc{u!}%N9gnik62V?lSh@98$>!XQyymi@jgapu7!ctg=!fmV96(`dSJqVJ&Ar zJJv8wbGUL?PNj4#wdn1nG3Oxg&+IKD5ZZ)fH|7?)Ul_K0=H%b}JS@(NbQf;Jb~<0S zIe|N#ivE}5Py}F(HK?!$D@^);{>s9#ew%?d`bcfOXpw%PjoFEtQI|jjv1Qq)Ww^i8+2>wdTvJG}wxy_*5}OVlt@Ff%P1(%5*; z{nI=XLG(V*7cY93ID;^D*966vFHn-?RSY~YdRMi{>7^V_>Nj#S1BVq&%$c6S(AgM<7jHaH~t{0 z=wLT#p$V+dXRl{TAYT5gLEd{m*;m8zq-O);RKMzN^Wqc)d81xr1+V;UY` zXUXC4v44N5%{~UU6MS0E=1+B|+1UC9`Wl}NI$fb#Zr>hF_OSz#b7;46KY2il2B{w& zQVWd|Rn@r1W+!L$3BRkzb5Z|kwE(wd+95rVzxJnhoU$QoET;Gv#-x&?%mt37utFou~|ke;DR7WW-+tcqQwec7aufh!B(eL zMuA((Hg}^WutV;rmfy+}%V3X~9XB-_L~D40N%7gbnlzf%yYrN?hmRlDj`qZ<9 zs()8O9D~Nv#*c>@cLjeeVoSYR6JO^=IPtv*$+R)RLa)SXQGi}BKSDB-oGK!Scsi4a z1Ob%CGI%diZO)e@1QuuX$01><#1DBRi8j-;EY(LZ(7gwee$Tuu?F}5J_f}p`<`;S3BmomF#6k*vbV;_DM^sCFg(Z(zPoVnW-mbFM zyfj$t%F`{g1s(U~jfzON9?@mt2s(?M6ym~tpI3pX%FA0RW(!-#{FqvK)cv2PDx}}JVUj@QcXd^wAAmAC=K^ux8i-qSK3ZL z3jnG;Kb5ri;m&r24#a37`S;d!)yU(vRrszF)NkcGPCfxB3A0l5+cB7jkk;mD7RDjg zVhn;+7d7z;jM>xFXHVJY`emQoVtOa~v~oUbos9()!ki^IZ{N6t>5)<3Q(LiOMrV1Q zVP<>mtD}a++QD$3Mdl-iJ`29Z5t%_A^ubNwwxHT3$t&X%Wq5Zst0si9JT|6)qL(}(@$%^yvmmN)kS0bP)Km|89aBo8r3i9 z_;pFDm-mIb->!rRZ|<`|zVAx&n!Wm~S(_B69@Sw8i}vnmt zhxqI#0DPl{A{aC0r6)I4y(5>E8%i0a1=x6o5LTgXt9YSA6Cc|mpYUu!CX&V&}Xr3YZ&eKmv5@P_e~#5LCOhu&+g`Xf&%Jx^!4ajz@Wx~0As z>fz&NlQ-#`tus@!s~KKsF8WeAv;8jPcYHOF%z3(fkw2+S8q2U{hmNO-*rL|!v8Noe zgkS4OJ)lP44nehVePd;H87yNF-%#nRz@pPkaiMwD#)sH|Bp!x;=*~xs=jVd7m^c^{ zI*`ucF$Czm(E`rTnd3}wLz)(+5%boIPBMMlt%+xeG^-m@@tQZ=d6|Q}BlX4ROY!>m z#XeAmQ0*k=;-v_LO>kp6@IN-zZ$wr1^$BJT$JtVO^6Jf46~5W z9AD>LUk~R1yID88)vvcRX132oE%h;-`2;tfy5#u`2r`8y2Vds)8xKeD z$OG1N174OxZ}7JeTnLzn$0THRsRo#ZNEgr#-nu~uhrP+D4E@t<;hlBk{byh#j;yI7 zqY4uw80mdQL2Fxx?vW4V5h@41Fp&?h)126nF#4fDU+0$0wx(yaD)u_R!xNVw4-##mKVRD^~>6#&(=<&HmqkOwM*`n#}*=~YeceL_&8-dYF(c}ggG%} z6$%fiFlGyq#xt_}xmmehn5ubddDJ1xox?M@_RG}tP;zB@3w)*L(Wb2S`0^iT_kEAY zAtKg*Axlg#7DEjC@!{1^8koG5Zh*?CZR;OlR=Of6xYYX#0ncl!Hd|zrYUTAxbQzf= zC3d>(EsBK~{;sQr8^m;2j+lKIP|nL7nvn7jbffb9Uh_q4?l|){gwJ3bj;)C@HONVI%&=#Ctm2QQOR5 zU0(Sy&-x?1lSsroR4$3!I--T=^I`)OBhBUL!2obHHT6-t3JYJ8Xh*aY^9r;d2Z&3w zvdx_fD@4#Zwv0X+U3JFS|p&8Pthz9ZkPwgz{ZDk0BTJ{$|gB(VerqRahX9 z{h?XlcpdctE}|kzI#oiJHKMt5y;ciTriI>OUGO5e;?jq?elu>^Q@R|a81Hz3VdUW} zHWiCn|9pr%G;NRPUc&1637lLFOlW^L4=(q|Ma}~FKmGY_=G;OD*u_t7Eup_z_YTWa&NxY3CPSTSu?m`AFhvArQfbrI@-4rz|4}x$>NWJ ztww#<6oB||=@~nad`t3AdWQUyo;3fayBwS?7%l!YR+F^DO$^k6@kAR1d_zs@dv!yS z;@S$=0ho4w&`F7)+KL$4LfHn}+16der`8k?Ys5FjHtkC#m1ugGdMqAWW^!09c9Pkd zaoUT_7Rldy^xQo5+%)~Ylxd)&A6-RCU!LlE+xgmtj}v^GBL!)Re4=sR#Q^Bs`f_&( zAu_7nk^qjJ+ktqD8n>v7`sc&d&d)3W#!*`IE*0_S&DxR5!~070F34JS$1K>q#!ntV zw`aFlryin*TH>w(z+(>q_FCgsg7G@x!a=1T$3erfHj-kq)EKZ`uv<;DB9q+d*c;gD zb>|MaxI+Zo40RsXdQ9%|JA2k*mUejxg`EF>78GrF*(O? zEV^H_BWDlbV0%u7(+>9-D_>!IXu@jCUwL|d?`OTH3VmHCTyHDw>;)D2k%qm%UtRF> zvH9i$ftE^rm>mj&B~T{b#xz{k9^(BSW0SLsb z+<s?_q3j(0!r^=E{RtHemJ|nz%>L;HcPQ+7jN7~MZAtJq0nF* z7&#IMccG#PbwL!eWinD+*1mH_XJkUyjD5zQ+=`XGjx;ONGe z&Q!WMronb3+H|6%Z1RpuCd~vf0PPeVNN!a$<;r5~S}J97>J$L*`ty#)YBl-uSlNyR zKSo?4x(+3${@#?s_jfX8bN0qx^7G2V7GwpiTDtKI^+$X$E8OBKZ!xcyWmiRy;Yoj&)Cq5{-oZ12f(Mo;LROWfI5Tl7GtFz9Yx)` zS>q~SZg_@w@Y(4l-s&y}8ZDqVxfcn;NRM2HA)QMDW!huv{V4zgh%2&vWkLo(5v z581pjV@XxQ@Zf!EBvxFRo6Rc+rG>3t_myl^TYgE}{X0L$Ju69J8;~hkg-m<4EX-<{ z1l~Xvpev&q>`EfS;AS79j`ma_jOJf2v#Vlk3MWcrq!}t?JpwHmCl(_`l*#qOKa51f zW*;oNF|}g}%)4e(?qSmy2zO+gc(h8KqE{F$AZ?2xqL)M!Y92+%uG zK7&)zdg#!OI=(MSZ0RuLe`rQuRQbcgY6oyHGO});OIXS6JW~Zw?0Y&PxmPUxvZ-xF zSd@Pk6Vrdg*m#`sh>D>vzIN=rr)TV8b*wHOj*jN%D>aMS&+knP zoKe>J*gtI_H(pNxLuuz4ry&~JfY(O(Lx%NCJ^Sp%|-vSf zRWUwW8lkR$#**kBRf!XFCv_%A9ch(+PLuF!3i9B1cXl|k3CZ&~*tDKOZ`|)TyH7!} zSf&fluyw=f*FX(nKC$|#z!BFPWoD2trjGttLiXu zoe(6aiTj9kCmqOhBTEw^!^DFPX;!bZ1{0M&@Ft#tPPgzgg}%lZfE zsB9;mlk2DvUycFN^`4DwVoSwP4)nt+D#^~sViFbUcQupefaoLjcSZ@nXp<58>io*b zS$<)-@LpR^T9@?t6J!%HXp$u>M@OIj{Xrv;>Y|MaCH1y5;W=~kJ%VE=^e4B~@A3xO zD+pP*bH-w>zLc_I^|fF2Iw>iL=qjcNADTlN;wiHebc1$(q{<7jv~zk(BlnA2iS#RM zZGIu2@rik))@);N1#x`ayx85_&E%V4^c%PFLpVNpaS3=eI(jS`s$2(X+m0LBKw_E! z9i1T4F1rbre|zcj%nL+KDkO~_^;}`QZU;cNpI+SaMdPEH*%i998DsD&T7ii{OW1Q<1`_C|{bIA602eO5%^gFe%23<=)7T@&zckeAxrwQW^i@@3MtPi`R4Z69{vnoTw}_k$@=>uV@e)=*L=M> z2Lq9%u7y>t6RYS4{A@S8Qn?8(ca!R3R#|Hm&6X#H{joQiX5R2z%HB_LIKGW%d6c}{ zpWR_sbH(@@zA4!7+p#>9THM$ZhFa^sh##1IH)5q|&y`r0a42(@lyH@Zxi+eTH^Bc^ znAbj@9XOC6AoK7^vEVpK8e%9wO*BpPzx*u3%+Zll+ErB5O`%jn&DDWaVA|GV5D?8R zZc@2MuIUj;FyUJsoonmeYktk{;~5yXghYx)69;v!yKi3n zJ-lwF34sP52z@xZxgk842j+sD{F{%E$OE8+h>%K=q5}Vit8a>~EbNv|$F_}*ZQJOi zW81c|)3LQ<+qOH}u{ySGb)3%4f9@UQo^zh|_p)Es`o@|yt7_I1APgjd@p4I*2E;$# z(mcgC9KdqpbNg>dK%9t8qKpBHnoy;Glkn-1AnfoUYM>0ec7X| z_CFEMHPVk`!OiV3=7EY?4hWMofGN8k=EIZJj%_X~?2I|M>+7?8$r8oixz+lN_1{fa zZ=E)lOu`URR&~h9E}gAIhd0G` zf=0(c`#Z$rMCnu02fmyDZW+Q)Xl)y3-t-b^L6X*LE3}@>mTK1&rZ6~X0=|dJ>RK%! zfh-s7A~HE%)+a%OGYM*CkTx>z4IhlmPUD1<&%C4MW{-XZDrf9uQ~7)4jvl?Uaj+9_ zZF(w5d6#K#`fMy@C;upC1k59qUSPCLT8+1soQCMqswfn%v~FsQ$H^^wa}sPra}{B> zW+@NKomu#$xxLn;qcE3yp)psu{adQk3iZK4+6ZaT0r7P!z^ZcpbjHtu!v4{CQ98i6 zzpm1W25gPeys3;@zTq>k=;y25L!4ROMZ$92tBz`G_s%lE-qHW&2oM)*BEq`ftwy=7 zdIlqz3+qrrb3h7?eRfg=eaUwq>gR><88yE+W;MUaIyJvoN55wckhkCQAQaa2J+d&V z7&$mq|`*01LCREvOcRn`6j0$7`{@V% zW3RQ;^?4MXD|O8+w!4tG^{Xkypn3m04!N+y?nl=$a)eQq`P zY&jC;jmmGHW*0>AW*bDxf>%~AG4*lW3gz>~7BoUO>olWLX4aP%!0et#aU z+ei`?S?a}C;sH96q4a70ta)z>l?g3S>gh4cmqwXPNB#T z5xAwow#Z~bUpoL<=uiN2=7^GlhmYdlJKX#9D+l(2ftdMHxd~atsEVv9ns%`> z%r$o$fG%~>5M+XKrk~I6>#8u^Pz+RC1kXyQJETj6k1?W%PlbDroBOdv`^*_)cu2X& z6nX)zzU92J==(2^K$YRzBO|%J6>DO%qRfdltDV87EF|oCTfAVk*lae%c|(3z_m6H# zj78EU+_@-a3lgjfoLw<_#Lqnx)ugzDM~;$P0Ak5CxOJ^DhQm*A$3i@SaWt$^l~y2j z1MIp4zYa*KIXb`&hq_)Iv5-O)O#k6qKxWdmg5*(wI(>bpl)-ys&4cZ)0m=l%`23Y< z`(i8}hRQ#gw4GTG18t`K0wDq@{4!NL{dUG3oyY_J5#QluJyP3#MoMm=Q#eYLzIPXJ za0L~yFO+f18-a(d>EpdAUQ9pu)^it~i*ZAJgH2E;Vgr2suM7|yQk#TkV62=9prv(9 zx7j&nD$8~|81sET*es0J2A>Bqw);f#OxgEG~#J?Ch=S{}>xNmL%WhIV%y`b)q@CH;?^ zJ5$c`3sf%rjH@9c#8fc2kifIJZZ?d5jMh}3sV!e-c;M6L2JKBVXD;a2v~nB9HC z&QfFRcLokMI$&Qy1MGA9wHKt&|t2d3`ad1`N=k4AKtBP0&IAP+8i&U5~tRXw1 zqr7AvU+@#Q`!P%FQv7@%P<-$S5qq$Je2O_o#%2i5P@Y^cUqE3X6OE)dw=iYEehH3^ zNj0S?a%d(tHm}CI|KT?yA%0Ag{g2|wPFZ(g$Hz!P#>sf-?>u3s3)Ez^s$5T z!Tf<>zs(OTYb40D*Wr52`^0eHdz{jyqIHnCgZ+fq%!xv{M)r zgp37{B*L~MqU`<-BYoxU?nP78UcA>B$EEiN4Wv$#H+Wp~ih$o+zAxWS70!JAMcUv8 zeR6LS6r9&tKX=BZ-69dy?PTFg9qj44Y*(q=Rfaj*di@ymhZ0{`fk6a2Rms| zH;czf_9o!z!%L*)aj6phPZf9%g_YFvj8tw%?D0a$(#m{yzpI6cvDN$2g;m)o zcKMQ^hvc>z>n0k^p;D&ww8=j~){L>Gg4B+TL`C(+>)lRzUkXLuq^|dMdyshKn?2wv z?O}!exbcQSJWmq{2`1 z*}Bq#=@>Sxp-8t4eK6apr(69m(I&vAN+hp435&AAP5oU$^vz-=`4Db|4PmnE0<)Dj zx6r)#0{(a?sa!?M3uin!ItUVGI+mTXN-rCW?V~lxlzHre>Y~w2ZXmBnv+IKPr1sFY z^de=PxB$l@aVvNz;^k@$J%!UGvRWaPAKd%QbLm1=qsTkvz-$6)9>X@nGBaudM6q*7|S?g>lujD(c^pRb?lERX*Yg6_b7-O2(USs0dCM&QAb=2sKcrYc6 zbcT?2TIj=Y;rSMXt^e3PCnKv7I;jg%Li!pF)O33PhSl=<(>Hf1m5C zyRn7%B5aVdhh?#Tv)>@sHUT)ecL|?5Fgv7lWTZ|1a#WfHZcpr@(EYR8(SIN7)6S-V zyb}!b2UmMv?K5@##02mOG!-gSgF=ws9viilc=rqfb)9_ib5Uhc!!K&qD4@*|dF zQrBj!tfE`9CzvqNFI>BdIBY6(ZI!fp?S3G1l16FoiG;Xf{rvsgcpO0Svo4Vx0Y}S` zZj1Zon^5sCgcYgRkLHR_pX*>ALe*LAcwPQv-Nx0O3?~M-y75PKD3~mcrc(Eq{-}nqJhPLT#p-?|(VmiW%ShNTf_t>Tk?DO9urB&L5{twXVf>_dt4E z&L3Nz9`m9t;k2;cTqpoQuyGHZ`}Zryuv>}yQWd2do0)ql)EXa6<$7P;2u7l^u8y$H zu$9TEwDF5(cY{K|%dpboor18Zt+g7ytcX=`qTl{u=*Povs9MztMDW^x-Jd`U5eSP=neroe$r6A<}|wX`VB4Q{^19Xe>?zDRD*(lzHf9?T*6dp z`|qJ`>AiZF;EJJkdGq?j+z)m)LEb3W3NO_M5VEfF@WzjAjaf!?evfnZOQHw*dg+TC z_t7Q)>R*RNMBJ1@(Cp9Z4KCyH5Z{V?WLYtgxM5-b`F2u^GeUd0$uctk+zzlmdl;ns zrvQz3GZLVW`9CfIQ>l6BkSoWtOEpz8Z7yH{!|?R0Rmz{tz# zl+m~_90=KFDaD!+55L+-nyov`$^ywAPs-jPwQPm}?G_dP5}mXkQf9kdAVF&vEL zMlDVhzP8*_jLlm(BJNxh6wVee4eq$#;NWk0V16I5RpV)_$$B@ed*X-EWh#huaVxnO zAw;;W;~_=2;`{OYsrYIa{t+bpln)ZedO+S5|8#M-$Q^j|>z|KI=G-)0lB@W+PZ>0{0yf&!DjS`vDLe-UuDzr6>tBMQjF&Yc*|b)`QFmHH*fqP*<{ z=?LpKoFRnY(EdBXvpwupdb6w!HV_)rMz~bvnbA%#7{1k>7JG1yS(1jkW`Wbc*+oj= z$dHb6n>a$vcR;mTE+-h0&j0*hW}u$LgSPU@_6g{6RwaXIqx)d_12|5wT?`QkmZ}YE5jsonB z`hxX1wCg!6I+dP z)WP5L< z2@K2kQ}4ucuT9JVpAkx&@Bki#|N)ajA`b4VC{NEP^snp1@WPOI|()bziS3NZyC5TfZ@ zBkXIoXHgD|+}4FM#AIOqScz4l%?*v1<1O18r6GzJ0S2}UnK}Lpk?h2?ZD}X`Ol^KJ zKHlQ>z3y;5+S(Kb0RBNcMNmM%U7W0NqV~sqBbrHh67%LI@+N{Jk___#{ngVpk2tz3 zL)6*4b_!bzJHG`2jO(T(Cu}_7WV!v;&vJDJ)ZgIB$7Zeq3l?rdP5B1S+&Zj1^_!N6 zWsFxFubzBTmzB-jxA~97T$C+V+9&_EeCA!BP)Vd{ROwN2Z4y%;j&uxXxI1L%Pq#y&9uSa*VqZ4LS+`?q0OF?&vu zQ}C*3L(h4CwbEFdu^9L_cpG;1a?du5I8~kUXpBeO@0j2Fe7jvBVmO%--YDG@p` zDX|#Be&MVHP|OlTDnNTb;1d=1a9y4We4R7tpJeIQ<7s~JP9kk$7nk(>N2Nwb*s}0o ztNlv<#S5Kmqmju^6>!RS;cH8AX|#jAi7H}xTE)T`;nO!$jeOIRjJnvAm&s%(nyyjK zEqs&Gpy+x*NQ}pc$iJ=ECTs55t$HPT^j5{KnIV@cIOnqP*A^>U<@d?XqNCRmYwS9j3|9segU9wlL z6S)5zRK(zj%7N$FvH*Rhc_Yk`&l#@Hnsc*!(q1;4OqAZAuYj((#cO_b^k z&P~-h>}}@m+hKwaQUjSnirWE{op83?GDcSd`QcQmOJ1d~?4adlz`uX5NckaHSgZ%? zL&_0A4$8$UI`s5ZOG(-(3049SCp2iy9Z6n-b%f<%1(@fw4)}A4l-9);{3rsuX~5pG ze}X)yx7t_*s+^)PHP~Q{-OE(3a#p4#5PSNdiW5nt9giYIe8L_SYb6nJ0FpIYsMF+$ zTDSLVt}k7d`K=cNcN<#O#F14B-L>i?Rq9;iN=hryQL67d zwz<#QEd!*L0gZ=g?^w*4dTP-GwLcstIvB%P+N#)oQ!wKtrCN#Fxr?|3J}!TRP%be# zi+;P>3g5Z!4aOdkI2=J$O0o`GgG18*dFaI7Rj4RWy*(V>j zM8XuFJXjQnO?d)}Ov7B;#VfgQ^i|W0EgA)xW6oMk>PivP$Tp|&tX~5`WSZ7mioxHZ z$au;4@W?NPtjT<+?k>Oz{C24a-ipIU3C)P9W;}6?t_vDbcw{Y$dU+KSA(fJ}q?WB? zpk_VJmhq#U+`>}-c?U?!40)isSaWntglv1uP z5KJzr!sq?ONz%Z77?THXkWJKY*F)5Qw3-=ki>t3})K3`fm$*Rz4J5T^*rhok@Fc8w zD6P<5FzAgT?{G8*B-oeYKoIv7&}g?}RgiS^L|GfEg= z*_#%f=En8~cSS}mK{^FL&1gwwU@vS55~!6T`L zn*hnTQyw*9wl+L&TUk%N-q-7nTMr{M%-dj?x08LSM@YCfe;V>l{{)6nwxsi=s%P3M zk3_|(hy7wa9W}}#OU)BDnjLolY?|O!$c*?b3e@Us5h%CNvgtV64hW&7U+ZA2^h3-NECCNzB|cvHwqmT^&2&);4^Nu41ZpI~@lkLH7DqNao}=Jc}( ziUmvUVGC*_>A}f&8LwrSS?i!0wnKg@AdkS-K+uh<$|UAlK{yfY|8$kSlL!|klX5mH zbNZG3_^tC1O~=mn_Ik53RWO*fActaFch&VUm#ns>SIl>U35^*Q;FG^-?U(mYyib~S zawOq!Km8D6^j;S81yjSD>U;B&13sF{*7s7eB>r}BVrrMF(H*8g6XfB(2i6rt|6?Nh+fG*m{?ZMUjDM%8}R$ebnQ3a{AgHZ<*^up zv^NfNH{Lce?*84rLhm3>0kw>B0T%&Nzb7QdOuTtcXIgb)NTF7W!YML>R~FbL5Lw3C92f6a0d0KsRGyfUOtiAy&X|GoUsVL|7=; zFO9Oy1T$BZZ_%^OWQE0HA_NLC&1=S<3eeo_>lv$%7a%#^%T0q>E)CxX*fpPDa_C~+ z3kLOtf0qiCJ|Deb1^i#P^J)YwEj*gCHtQpW-;T5HbFMdfUEij!K%cpQZzB9#DGgAb zwfU4}MHr^&00lfUtr*$6J8uI!140A6QYBej6jQMQG`!`VazrRk)`w7*UaeouyAuj- zZ3=t82iASR`>7B6_zA}Nn%fQfv-SRRyc=sX^CQ?c9=Hy>6&&6${B^w#)BXN)ApZH8 zheVikAD1}!#uU%Je&87nK<({K6O(uMWQ-TTW2x|V4}gCMf0BQutnyYHsQcp0epVOT zY8?gI5*$T@07F*OLJREctj|1@#9RqC(X1|;E)ti}bfc*smb3<}E*pl2IoP)>(_aIo zJlwjdvH;;C9d)8RXA||W0uuUYS<=`>w`xO8xRE83&YYTQPEqat7|^(6vnm@)3_e`$ zdh*53fYcQK@D}G9&O`jjC80s*Q#()~tBH-nI8{F#?`b<+OxV9#&UZ^#3Uhh*{Ke}q&Ejwa60gRY|#d#ve`U8t800Re9p=DNwmR%4MWAf>TPx_N`TS zU%0#Lx-w2z!%q+`UiypT(aAD5pV}KV9!d(0qQb=^i0o{CqMT}HcW%n>CQ;aTH{Z|o z117Ky_`Fxg{f*#EXpA7fRGm|9-0Y+CJ1ZR?EBMEJ&3>fz!~$x_rOEKd_12c64a?_~ zWq6MQ>3$76O(!A`nw7|S+7q$1AW=j(4kiaxKA;CliOfK9Uuz-Lwk;2`c}gCy+hIOH z4Qr@rYW{o6_A6-^U3xz+Toe4XsbasYm(;*Lyg^?ye85Qm$y%0NJM^NBRA-SQ;H0(J z?1g#v3H@LGPW(l%@UB?g^~as^4O`d8jF)7;WH()X02!z@IWgj#>+cFgQD@~2=X+?{ zG89+wB3>N+GO(4HB5!EXg4_q9PR2{MUzg$C{#|`w8*;%UrE&*-3$6VT;?d?;9A#@9 zVd)~2)6~S7DEN;FYtn~O9A^JtfX=-hHY<*WHEYH)JKXv5c}M0ODWVHqE%ygIEH9r* z_#Q_Ffr2cGtMam{G=sDays%jvf!;v=nlOVLSV5`H$Pms%@A#93R76W|oELM9!QW+l z{ggZupSK2;5cJJj3ZB`SkJDT>tAg@NDR^M;UM{;OcLqfG2xHc#We=_3fH>xV&4;m2 z(Sm(yy>ZSsryJcI&}?xgT(~jKiLYVr1sfAIhn=h3~}m)+7RyAB~MQL~Q(i!1~EBQ{GDVC0iV+|wf^BIPgKL7Pn_ zwhgkZ>yhBLRE0CDk^t9!K)+L>MW^Yvk3uY7B@&Msw(ABu_ZvxlugsH4Qpm zGbd2(^zCQh^p!ybkbHetV^TO)d{c-e4&Tf7OJWIHjb~ndpd#`7LGaZ7ZY1s7j*fO2EHm4cghFX5JV`&>4%@?QArz;X-!CM{}~$P`yC;mPSuCqB);biKVs zmG?hEuDN_59L#bI0ZM2&K7o>at7o~~$x&L%ZZFzs;q2Yt080B}uRuasMQrnnQP~A^ z?GxXc&irMKn}38{uR7d+U+hQ<|G49mJO<8$Di%8inDc>aV#W0_Y-0FkS=^#&=t}l2 z23geEz7k}WDZ8G10A5UNnDc}4lU!E>!ujMzUS$S33K{J2V-u-JEX0CJMY|dQ^}1u* zYr}IDV>LhB1Aau*?k~K3e*c+^GT@F09f6=lVNhD#BhvuDYKvZGAu||pzJ;a&WqE0J zhXVKP_0ZF9a=leyX*#GpTTsXk;MDp zKDR%(&)OT%%5as^jIa$q8owi(G&fXq@Tuu@K{#88I;NA{HecPIvT0ZBpf0xE4G)9ayS~!OZE8AreI-}I)ZQaGK4ULJPf5IU9M{t2_?hofs#!NS; zrL(iP)Y((ttzeh#=2fPs4VmC~E0hjeJR|WSMc=`SXyrF?U*0=zNVx7zB7Wy zyfYn7-OYg7fDh=OeQ_}yM`jOqnkYw<45O$GG^TJ9=mv6HiHUn}m{RFn=jQw>BV^EE1Xjs%Y?z#;w2~koO)rC zTMd>g8esj;!_B-#Y z=v#C&yF~7XQ(^#CJ5U&db*(|G-lhxE49_2a(|8amX3UP@pqJhfk3({S({)F?M?<~~ zqPTKN5q7*I0-e78QxGXf`A3JJE`gf+R#MvrrI9hNL%d_ATWKJZXt1dDjC$e4A0*95 z=~z(9ANb>1RUotJuU^L<4gvoMcTBRI)S^wWOEn$q+5|whQ20$SH zus~x0$UG4`<&*cE(WK_u$!s#`Z?_2i(s$IIOk|hS9_33QgyQF3Uk3?+V86AfrD#YT zR+`xy$o}|FLY8pljtAKI4`B5Uob1LuZ>~vttX$) z8Hi{3M7e5Tw)NRIN%-4bE3103LNnaVtr}TJUVT{C&SnTmf(! zfTO9PTel~5wsVh2c7=~PmSXu1HKV=3SPk|mT)q@XH&zpF+`gMLVUBulk1fZ>Wo4 zsrJ%lue)40QN|pbHtb=UNZ-V%nX#`Oe3=Z7H9{XhVuxG4EjG}zN{|d7ziNdgU?LT$ zt2B>3El}Q}$rSo?G<-DE|6x+-H8h1uXl@V;X}%yH0IWR|9}w{+B#>tXDX{-TV|#-P z^_Sv)3le(7DmziALN@}l1Oreqnf;T~qkW-QR){FHbY^PjlQ%Gv-Cb_+B_|)~{n{e% zmt)7AC%zvn10kWYHGVXUV&~f3r~|rJWL&M^^`HdUC+z1`nfcQAZxAA}=L7?2B$zb#RiZ zojS`IAl*}SL>K39P|9JcB{q_5z@c7dVvGK`^qZ){0cnxpsFYBbJ9?1XTTJ=V#9Wp= z=M;vLcn!54pL4+_gDMN(9YBHS%%(g$?yc+DFC!DSsj59I*P5ysI#z9ywICO9ZqeH) zlCvVi4clp@n=)5@#URJC0lU^1-BDPBjmVhB{jgEq(p;Pw>b$u?wK?j5{s-;LS_ec| zIR%Shm2O&u5+@)|PejFQfzPDDQ&Ln*#pdwU>Bi`*>9(0HZ)JMS1+dw8TeqBk1box` zS*tqIh{w6daCR|}i(^_{R}e2RrsVI`+6OVq5RXkXS-YySKxVLzJ*EzBFM|?7(_E*# zOm1hQ%$WDsPGL+#L_Xc8bLJx8rkG&+L;7vrmQ59$C&PvMUYOwfRq3MNc?CgBiwnre zLRZXjM$N4#9}e1W4&a&7RCHQj$626&zGOXEe+AXhS_Z>Itoxy<2`j^lr_=Ynm(N&7 zbijhU?PiE}!LWR48#|_($X4K}t6jK@eiEh9EyT`VkMDB(rYWfwGayK$+WdMcCEdme_9T714F8M$3Paf0xMQ0OL|p4JVs?mZ}a7Gw3P zdH8p}VqG)kXn=YBv+xr5$nfQ5Mneb`}?lo?N^$lAakRlcH5%f zAE;B?PxpD*PKOX%-F1qTlb? zmjp$dOB0*uBV^lMoS=x%iA@MX+lTHWBPW?75Nk!O2QP{M|6U`v))Mz!QB3@fz6bUKsOp~!{)u)_= z>ojR|@&Rc!8oXB_Q+(ep8*CF?qiK@gHuBY}&%43YsVNqE!hpFzTRkXPULaPA*TpfG z=OTnawzK|UwJE>q#^nrrje^sshFq7y?&&gE*BE@``NY+TA)EYGk$iE1Tm+-58*o{k z&4sa73q|Cv=S*JSVC9*yJcEHXcDKe;{rzf#Cx9!Bb_0{?Aj9&3YjM-W$}`d4>Q%3~ zUFH-OceLjK%_;mC@eJ2r)SlSZp4u_C)&;O!Qp$df;gun#E1%VHeTLZp50eBuV&mL8 zv?J|*`op|>)ZRx;p%*&|TQEvo$n9^sYj{RWDyCnf20yBFjy zIskW%0eTN^*BBSY-f%+i5Sh1lB#A8eg|79wr;?+9l{ONc=M4L^W0(7bq=JK^jEyPm zF`}C!`8NgpJCU)BQH)}-X@~g?{62~|uVH^;s50hN9)BI+DMT#?k$%T*{g#Ux^@gw6 zoNJ*smMk1~cYEud4Z4X1)H=;lZMkA?9}7A&32?CiTwU&EMP&=5OJG?r_PXzvGRLXe z`H%U30+(z%y_quLnVS>fv*>rRQmD`~#A{KMC)YPL1ZxlC>71_P+zCO?fA1e3h&!l; zerENjgP_vzq;UJNiwd>5Y*w>MUwh~_v?oaMUfPr|3o^gh_PwG+2FPOLP&v!i`g*_E zws$PVc=vH^RBO{Y2X>QfW43OiMMaF8JokGL&B&BixM;aIGrA~%J4uw{>Z#*wRj8M4_cG6c$ps^4R7>IcEXHqXp12Rk=I*sEF9{)PXD)wvb%1eG+))AsHY zuE4pbi!O)Ex{6Bx8U>-|BK0m{@H*+x*w?`<^G8yxN3a4-wT+JB*#7<&BtG_|f z6az8>u^J7jsWoNqNH!?r<|lT>(|;4G8BhbLB#a;b!~J6YH<_1$BkU*L7j4>(3T$x1 zO7g@5SpQ!e+IqwnmKko8G^yq>3SlVX`Jn@YrgO2<&${!yczDHr?prMAX8~Ce8ml$1 zRyM6zKFLAxDTT=)>!ISZGoaJYHt%0pzRM~KDjv1j`1MaW#?rCqd2&2iCBt?nl$4nN`Sr&FC5x!+{sFKN|H#~{2MIWvg1 z&I}7(`5MGkAQhPO1Rsm5`n+kUk?`7SqjAo()+j&g)1b?V(@bzIn*N4?2w&On=$ z4)j$+a)D?F`wtr}XX)xqdp^>OOFK=a@+oh+O5osf(nna!tW?=5_MulNR-VAsWOb}u z=vG4eeL5}+bp}tp{$2&(0N%12ASSd?8&{}~)Mbt-lU%znqoRf(eVTnQCuk_S$0~V- zAdd3!pK-V}t2k_EYdXBxQee4h1~vd6QcVakIyB<<)$vi@GZf0nqeeNZ#SRgyZ= zto^-`{$g!f6?X&~)R+eTlIFP@A<8UCm#7m(mF<bMoVQGW0@S|VPUx)YIh$p z(Rzb}Q7@6aRLho|}79v2Q}hgeIOU0UfRXVXUe(3p`q z)M_8jdcFWYAv^9dh_QMBP;l|y`H-yew2(@cRNf|ckITE^(T)_TFV#RJn9DuR=}J($ zn)QeC+8^fcd5ADUzVDO4r|`~MT!%?r#$T^s`(VmGe%D2d%giRL+}0s`#nuiIG}&`6 zT*t)OA4ycPXXwJ%d~u}9k3r)4(!?kf zj&IsTZY2K3G(fR612X%wqd6{br;HF%oNsyuj)zcPNUzj>Gr2vp%{dwoDPNcd(Ih&fG;2xfiEB!;ZmZt zp_nuq?NF(ChG<%2;#04ILgz|q-jd-@!1%XOGInv|63?w28A%5nm}cqk`c~cfv1s%2 z=%a5X^;L)Y<}*j8sJw~smTNg}d5{*UU3UER(EbAGPW;Rt07vR9Lj+Uz#cE9MW8L%a zwSTT?hRyV~#&8e55G=6ZzQNE`ztSLy!h>Uij67Ntxz}7-B8Q?ukuDjo;{-&>&LjW} zsuu{Jn2`2$Qo1WK+}&Kw&IaLO!E0yJPgIUtl>MIMZ5W!5hy1&P!;2p@tLv~++Y--w zCK?p9lB|im6Dyhlgy4-hqDXfSi+uPxw$ zy#_imD$X-j3~#;?Dh+A}*gk{l>+Sb5vHZV%HOYCWk74;<6v%>l-nDZjNFI$7W}fzm z74{6Ycmi=|<8ho(naN_M%LQe>wkQ6MFAT)BU=3jv0E`2`s3QHcQ_=omc{NbuQiPWs ztS9uvVDP}noU&$-7Lf2&#Xm?78g#vz=pQbKFcEACzRK!<&YRw}yIGUm{RB z$^LzN+fHl+Pbm{7ZTkCO_OU|7TGp+zv#n>Um)9GMId+j17vmceF9oTeG#(yupo%AW z!_};}2goI8_hd@rAx%?jw#FgeZohxpKzjJy6uOQ0Nr4~stVv1HMLmLyM+WeAc>w!E zOfd|@I;EO;%hNQzgZc#3n7O0Mf{T{Hh3kjUp4897II}BD{^#VFHa9#=3rnX7Nh7ad z2=2VeCycs~PH>fi+sR4sr`*FSsV~Km;zA~r2cc23C8BTNSy1!pHC$Du9fiPfpjl$Y-b-J-#|H%b?gb|Fk z3b2)yu-qKl{e8f-PEUq5eG9LbFEI0+dF=aq?qF$%KtQKg#t;ZrIRJa+Md6UrRYa}% zW@*d6w!*WhR~+SQW`(1Er^suvu8=7k74#F<_1b}UhupV*MNOn9F@M?<3aZ= zVP$(@BIYDMJDU?PjHffhF%43Guk-t{q(qO-!)(Mnvt=kbB`c2D!WJVfT5Fp1B`9D+ zdY-XMS&9G|tXb?{4kQ0cD&OJM;HLAtef59$=>Lb6Y{Cne{f73R6Pul8tK1hq3HjBv zO8plIK@97;t~0e&p5 z)-n7W;O$=pA>`CygvD_lG82ZhP+DSvq+emQY;agLJNA$Z+&-s zn)O!-L)(rwio)@SjSB7r$7Es$d!N({VKru|FK|Ws8`H3ugun_lnAqop}t$SgP zEPpfu5=rEl<_+_KlbQ98Gg75JzLz7yK%|3$JUCfg%6M2J>a4m&W>o&Qmzbexg}2+) zAlf=Kfza9V+P`JR{?npE*0DV6JFE6>8q=ugMn4heIYqgAgT`Z|64zlqYdY$w%GC6 zJLzKSz=TTv4O*K#+Ke&~ozyNYA}R?Cx*s+o?SKj|eKI5i4mFfcu0|)V5)10wpro~y z297nV#l3E+b?wrm|$-b^H`6vl5UhBWS1TGj-4G+vgyzn>Ry- z@(OwvQWwGqHAzy`ygdL@N=!6)$)=)Eg%ChexqASc0n9$T<0TzWc^5l*Uj))-5|n-oTb?zC99!$v%(Laz-GayBoHb|Lx>X)Z ziZyHPQ}X3dXlPZ=d$To#q*?o=D&oKn>IhNvO@Qd!*<3vWJ;s^n4_xD>kMF$abSrLO zUYc{60kAi8vF9c0q`I}pH~5LZ_JPdF+p+j{^QUaaerPx{jy>&#dOnBvj<7eNLQSAz zjbG#g7qk~p%d%-Y?G8JK*_6FcR}%fXNc$MO6?}sZXpi5``V)qE8|Q)`HYE4F=&rQRHc7i|a`M2dMGg|cdZ zL6gJbSr{Wy%U}A|>I(ff2T&-ILV^1FWR8WpCL3Wzl&<~KRVAkdx{4$YYi!i&0MoVN zEpCdazN1GF)e5ddbmmws4~Jf&VAsk&Jk}z6b*^B^DwEsR%GUN(^oGhzid{PCHuc?d zbA&D{TCX@Rio})L*Bgf_5GxLbAvh9X!7KGQi|UGr`*4kg>I-<6h}X*O7C&K?ia5cZ zF=Fsv+Ih<4J)6_j-H;AWlm5Mbpj|7Tj^r^_mCoETT^!p63GIl**~Ny%s5%TWyB4}7 zJE^6t+xmA8=$^4hmZ9`lxX#tw#7p@(7hdwQ0H}^&gp6*LT%KClgQugrX;Rn%G2&Mq~wV;?^ zU7F<0CSOzQ6I~I^)^)2AuJv3;7niJ|;s#!PYz!3SDMF*KlBc_NoM@go9Z?e z8Qt#umS!t0=>@s?D?9mB6AzC7NNL4M$=aNDYsTKL<08=Dzp`cI7NlVAoPEYfr5wK1 z(Mwe&Zq*DTS#Wb1uoH*#_n^^7S-80}+4;G=^3En9V}FYkWk6p&S+b&R*onDF1Pu-{ z_o%WQ7ly1tQ-`q$s;;j_!nQTRM>-BQqWUN>Y?**mWzFKFv;x~S4x<(TGNNHxRReH| z3r(A6&~_Ib6#6R*mXbi{m!1KK=*C3vPW)8$hT3At{Hl;qk!*3IGoDckXGI%a4vMCb zFsEMF>R(Iq##a`ZskWlaH#xaQv$CVu9TmGClkK&++z+=0ozq>6*is>kOk-MxIMEW- z7KhGl2_E0*N#fv~U8s@(HCvrX3Xo3=X5^Pq8-hDgAt_8|@48(&x*+O(82e_usZoy_ z6(O^{YY0S~v`CYQ4bG%7W4gZ;?0h*i8K8UgWlmxnYQ5NPc=~Sv4Z&KI=+MxcvYvQu zfmbXi2yRuPhQ<8pRK;F(S8^U6l*XzP7xitkJjSz6ywW4R>4W~6DP<;=Y(kgsLF=kqdQx^07Jj(^w z?fK&y+TUjYxIH<8=fD@w&M+i zUs*rO+$OZ}km1s^aWDy*LkBWpvK2JRQC+bK89^tdTW`wYj=S?jP;kZ(Vv`3!yKJO8 zNy>~S(m#CR)VcZo`xB2UN}{y;@G*g>e-=0U?-HqWEw}BfjAfz2O5`kbQAn>)A9Fip zLxD>7Xu)}aU22YirTn}bn#egI`MZ_RFN zof><*EDk5Np*I!Utlp7=e~&TSzDJo21(e1P9%vCpOLazPnf@upixZoj_F#v1-`hRf zJM!?V#v@A=@f$a&TxD}zqK2-%^oup*eC9+mGZ`y2_vLaoP$u}c-9s?nGtQVbA%_%@ zvmK}m2#=xMK$yl)vKmbb4i*2}yFxMGw72;Y`OEsM?6(*aOq9}{0IQ>y6W>XTN&8Fo zn?=9ct@V}cVE^02Q`{YJzN^^a$a_9VMu#YP3-to%^+!Z9&sAz&^M%k$O!k-?16)Pr z(2h{=Ed`nBP}F{hXZfR!=av3PCQP{thqZZFqj?|<4*Hl5aLid;ctg(k<3W1M6MkgzF zN_%o`+9<(L920xeD49$L0wg_*_GMmMWY}PPhkiDJf}8yho&sI9#<`XaVzNIu1p|~! z0J?W}ZoB2S^u%ck6SKcm?7p~~|FX-F7zyngJSH}`5mGdw;x#*~O6eI?w8xq^ra1FF zuKkXkhS)pzu*PhFY<)+gUrV*q zG=_&b7%4<*up0kAWt|0BR86?Y>0G)yq(i!;8$`NGknWO(rMs8z5RecM7NonC1}Oyr z=?*Cc?z&#T@ABRAJo}t|_Wb6ZIdk5fbI#1X{|QEn;VvCSOP-ivpkLSpI=$Z}Tp)Q_ zWG8@y>4czZ`0Vps3k};(LMy4zXZ8wT#>j=w^H9bl(`Sp`Zv9%Q=EuE3xRz|)QDeFj z

HdI+RaOUPQ4WyKrEgf`rqUTnKMwnFq(JMA*_lcvcx^S%2@g=wSYQswmOR0sF(e z*amJPu;VUaU49;u>LIaAjb;Y-ZE&uBV>b9R;TSvk0z zAk^<Sx&6L)P@{-{T7N=)GHS4AxI$bbv%02246NuL-NJ)OWfxy#A7<oJxE_L`Gi$$!`v`YIkewtP0p&8_d` z@g^iN`wN|NIWPH^AE(h5>2iuUsD*$ViD!+GWpmfk_mtU~A)?foMJQI`{)o!HI~R6c zD*6a9SyENuVC+C24o!RIx*a1n7VX#Vp_F~&|B=yD@{al z?Hk(kptpk@c9FjVfV`|&usk~6H+d9k9vAt2ZE~8a+0rew^#zsPHlAy_l-aEP?tVJ} z+wwUCsbEUsb83PJIOf+hXkV%O&Pn~}dWs7(t8XO@VBVc5Uu5f-SU1d4r2A%XwnyAa ziutc^_JINQ-z{VIKT(yDNRQCVw7M<+is+-7s5vM?B*0ar1QMvqzlB?k=cQZ|jpx^7 zH&)u7AvBQBNR9?B+p@maM>~v;OxS!;M;nO&t}=S>sfBvdf>?UE;MnB?Hef z()cqco0Wjb%IPS*S{IYkeT-sEWKkm(b^Nd+aCKuA3zqy#cHU;}zv^HEI}rlgbYb1F z&%+aRJ!H-V>PF$2IEtJ@29--}k$#;+p1;v!GIj~#*ozcLttkg+!t-)=w8QgYW$x`y z0!@}QFNeuSqnxKdIK+DEgi=kWt{#`bu3YU49+TOj@198o^uUb>7woqjNrV5SvxZf zn>5AdrJ6CUo>>m+cXo&E=(C>ox@Nn5IzM&Q9KW(@d_lP?CH_r!NB)=aUFIrW27Wt* z#K1{<5P>n`%k(4+^JDp4$CJ->KMyY)eUaC0U{#P8M57m1X~A7pI@Q6|^4Ld#l{93< zK>vkhwQuF87HNd?uI zUxBTFao+t7<&UOMfNM+ImH}YC2F@aq7eF(OjU11t5CXyT909Fivw2!ktfbAzlWIPg zrww$DvXlz6ehK|GCA+#X5QXbWf0)7>RjrVR@X^D0aEO@JiXNIRi<;9N>m8W~deL!^ zSi-2O8AZhka^1V`0k>PMlVNzTHFtT29oAdZlE$KY>7WCg+j#i;!@4+|fcEyzb>Ovz zYU77tkzL6pZT0VYh8vp0d+v70YT}Lj1WiJy-|U~s9d_W$g+Ed3WasOG3_YUA_H$E+ z;){Nt?02)h^n+cPznV#5U>5bOXy}v*R%HlOt$AKvuRF5&PJyQTCjtDBGGBc6RU&W1 zJ%YkR6yaonSx(pFz=Z%l4zOG_vkgcp*OHY*`6`$AplI85_Dx{`Kq>CYUgnrI5(;>` zs7qI}4cW8PNV1Ixxfg|C-Xu3-W@a+*8OGrdg-sX9eOm4?D+;yd{jv~6)ntryfA7kW z+up%mocomg!xO;!Yv8We(^%0K+18)yt8GWQ0_}MZiTB^?Gb7*g9Q+u%D18ew7{gW( z4EMZHBiB0;eQq+rqotE6C^urwbOx@ZL5c~p`#$V{asS5i7Db<(SSY5|#Tc(=T)K@+ zx5){dQDU5Mi3awYJVH#IDD2R zuo1_)C7W77X#GL{#_O&J2&0Cp4F7*d8cngprXk%uAPJzO>WmsB#P0*y{tgluQ`7`{ zhy;3SQE?QPxXSpbRD36d zuD+}aZLhd57j$*r!Ke+!AQ8-v)@SZAD(Z4o(Zu^;Fc4~`YO`^F-wSLQ5v^MH=I`?! zAi`*%Y2E}bS$%!|iO~q`VE=m6I-JJ+<7w_U{=V-X^*^7d(RAz?B5)}5-Fh}y#g?~k zrC2tv&$k$E_9pvUJV!Qs5laKywtN{o>pr%CU%Pn9LejxvrsmV|+ZVSS%@ z-yGCcyGRC0B)L4y$!+X2usA0rw;bsg1z;_Z!zisa2oK0U= zx8*J*<@ZbS>sNYO9Ls*Sns_#Gv(kEeSHWsW_mG#92+TmI)f!tRqOx zL%d<(c}{ML8rrnx9NT)=o&1h#X_JQ|_S2Kdc7NlKB~;ll^bwmfw>-y<+g<0rxho51 zn%sl0fdm3)dV?wvEw6r1_Z_%46_}k~f0Yyv^wA|LYkhH1cg{k8!{xs-xBpONw*KzA zcGq*DlufVkv6ceA;!uigEIn zw9C*!485R~iP8ByIkY#K84kB7w|vC3j)1L%`w_S!6rGIHDpPWyp+O~54fswESuHaa zQ<~;uMG{v;1I`A{yTTnNfBD;%BA}d-)xy;-OdScFvg15v9X4#KM*>~=_w#1rqGLHe z`xHBWS-I76JGTf_3w_q~%Fi_Rj&ACNVw9&x$%%0VQ}Lp8U*SnQqbxIbUAUX^opMqc zI7uX}JcNvdWEl~6JYKK>0Lk;Vq!Vv8u_n6_NtJnvGfRPm!9q4&|CxCvKV@~00_n%k zh4ka|{O>$tTwt2!;fvpMFlVruQX7p&GnleNw3pZ|nb#Sk21)Urz~TXr9xx8|(jB~V zz7Fh^TqE44$xu)ABcBw-d6ltBEhAjJZ?A~BABk>{e>xTy4}^WYzDQkDn9A^~ELl=N z!kt-bIy17=j0OY4(zbG%t;|I?-+0=^v=2T)@G1w;0&En$Pd3E#%K2=yv+U%aW!7yU z*|eR~VBY50Jj!W!AUdDJ?8rrDXClpMG4G&Tef+^Q>>>SFP|EXc@5|zPC;J9d7K+zl zkmRZwhKMDT|7L4%tL0^qz)>}?#O@?BV&RPBbn_b^PvJkhj|SV3jB?7>V3ueL2A2^vuk7vR*_7(ueA z6)bULaGJ9B6HUe0_h+DSJ9u+)$dBcda#VsDv9KF)@>X&J8^GM8!p6J$6$vW3*qYdgcy`wexcf}3k~*kp^5R9-=QpGA zlR0FiHaW$D@AS`!T^szP!i2m~JPQk7p`{mI356lda1Fm@`uH;U>;@Bn*NnuZ+GI8g ziZ=&-pK+9N6njgo$JO;DS((!88+uuw)UueIQbcH~HJF^{jsWz)A2IIWFS5oBz#-kb9Vzc`=pog0TOrQ|VEVgVeZ$V=&%G5K#8De}EVS(`1d`Y~I-WryVCJz?aT z# zT(mxR2L8+qxZbEW1tPm3iYo|4F4Lyd=MGS;R4X*cs?t}}j+GajPKlzDR4eKY0SB@9 zPj9e0CDH}1kWWexu$qwSM8jPD2N*@#eW=%IRv#4l=V{#l_ixJVFL6T!$QRAA)8mUr zIa8)QUU%q>RBZ*&n=&X;zrs&FOE?HhlfF#$T zV~bhW;hbZVagPDkCm}w~jd);;s@d;NIv8rQH(l-pqS(PmaZ{EfB$(7GPSA?YsYLZPw`4~p>waejA-t5 z@Tqhi5rOSJ-K@>e_&PtJSHXbQWaX)}%B45cuEzwU5b+T`k=4$_fY3=I%Am7oDC*INalcT(_J3>7~_da290kKdI9oj1dvz;R%_5TDtz_E8l+Hmk7&D=nGqamWR9 z^)zhcId|uL#d(WYk8%Ck9OHW4oR@%%c;SBD9tN%vgy0d=xs5{$OeF8|m@&W$=0TfE zOkP3JF62D4YWGeeyt-%fxza3%b?d?a@KaNmMVZ}A<5)x#)JcW%065lKn4hBruOZ11 z21u?>5XVk=T%f!%aS$J%8*7&|g%3uh*}Las=sD9O_HeZ|e85 zOMTrPnmN1R5_7$s{XYxY{T>bn<+T&ELvEwg4UYLYa8T)ortUiTOptSt5)g7PRmT?n z0GS!*`JSLC@Efu@PAfhd-*7n^E*Bj}_AGA?@WY`2(TBFJqtA)*r>MU+uvBE?>BNGt zEOQOh4W_eY*9*qSKV`X>d2Jot|N3!<9OP5YnADGfAzXiED^*gEL(0HaN`ovt07fF< zVX@+)<19Bfkxi!2ch6BdnguGSzv7O|Fq&fGMW|i2DzY*RA2RZ`3OL8IN~75l|ImJ5 zMCCzLW6VV7JLbmr@U*Y5!o|DUzoxg)q4l*LKRvw2JF7W-Rqwg7VYQKWu5G>H^mo*6 zhdoKS6HRW@_Z`}|Xw_JMekpJ7JQOTj0Jm&sg&ATVaxWJ|-`ls2tm*(&&cc9EX2e-6 zho`#jFWt-4nSS=Vkyf>o1Js|A*01H$#rMDVG}?}$_NiX{dAHLmBDPYd<7rrI<)nY+ z%`Lj4Us3oZ9GuXgH^~*nBkc4_YoIuRo~=r6>U|7fM7Y{!ymcpY6WiL&rVgJ158w)2 zxlshb!?^M685L>f%ApbvbE$Dab3BRWfy26Dof4jyp-)#tWYq++7%v5eC!51O36f98 z{kM1y@9<^zbB>Q%yQO>TUU+Gd8UF zG3o&;E~)o`&9@N;vW0hae6;;Os3oxHXg9b7JY`)HHJ9B8$87_d=lva(gzgW$ZB4tj6AbGzeF{V6%LSUF5j0_C?YjQ2^#KAvwM2P z20J0lFY|3QOMIr@bKqBU? zA)d?(jhOdq!2-sst0?zLUmCM}+N@H90Foj~&+}UnkskWEo8RdlrO5$YK>ml$HVcQ+OQ)adheL$dMM#d9Bad(R*rknaKCMy#I0R%mMSp)pSt* zQ(m!^i5#&zISC0ap(Wcl0~qDz@zdx`Huq{E$^r%nw~o+fq`Gle%|RrwG_EL>gxomn zCc!iE4FZ)9bcBRQJgM%qVWwqQ4_l>J(sU3&y~G|RHF^_@X$ra*j3;YVzfsiT~9 zU{jgl>$XWIz5s3}vWO!6#Y%%(dxV^GI+DY^s28M6R1$4bGd_e7k~2&Tm6qF7i>ly)GN)i_rp{+3LDJTMp*fVHZ`;?wX#WOvJt`$;y~#)_o4X;GW_A=g`BC?C^cI5LzNiqh_K&WSG(NN>16yrqO{a)M8E7#JW>MaXZ@*D?nrB|c zdCYs;WG2Qx9q~STOOHsz$)FeT5nEiiyI$LNmbcF&K;v_E!M)Us<(n+R5tcPtr*WVx z!{9#N44^0U*QX3h8_S#x5kwXxTJx;6%I#$SM8QdyE5x;oy}mSoD&!7qH|eM1I_!s> zMJ0q*s)0SbCO+l+@1r+Di240kq&iGk^w~2SeMN-@q%-z4u&#VY`?PsOEC`6kPi9-x6Zl4*(DtLQ& zapjw6B>)*_Gl?DmhI0_A9`ro$j0JcXEX|n58%t9eN(C7YAzP4@@%6q+_3s0cs5jD+ z^~pCcT-*vEP0DvPVO0>T)`?y?4g>}tlicD>^aNbP!=q0}T`*49+{~(?97;bOv?Yzg zG4W|l>@nW~Ux#Eo6xNKEEW4v=I&TU?_tE%YsbCkqgxQ> zb_iXG6^gvK%+QPNj6i*k>&OMnE>6aRJ)n+2pa@9)u4qoGfla61>eKw}ox-fUJiBM1 zPM1p`VIC(J(KK2gm+~Huwa3Vma@^C6SKEF@uKq>?k+Zc)p4PiU{`qX`dqec6MkW%u zCIifSIOIXzPpxSv`_P+Zn>l|JD2n;3+zF)oqI%pSG|*K}z+(JA;f)?*pO zu92_hNhTZ@EkY4&r}L?K{Sc3zY$-TaH< z`7o^xS1C@Bo=N&@PtjdUlenKXw@kNUen6$KFR6dw6+LrQ&9(B_3J?48UahFgH#uq{ z>w&toO1Kk6Is?n_t3-TmGOqOMYoyL?kC0Qn;i8(P@7m?l%VE8G{=01s6+1hc-w{*l zOpfzomDjGN5^_!6JQK1Lc!8?V(T=8w$~Vnsy^g{foGaB(r_97~i?LGf9`lL0tvL@! z;;@y3{@skacDOTdeq!1k?Yq{nh!A*^E=aA4BSP3_-;u1K0I%`^Po>VUC!DnTShV9h zJ}Wcbi3tl`KSUdd$A4+4g6`*YP-0UgyI@!*dUUiJlyJL!L`~bS7kC!-ZeZ*yQ@TT_ zA91XWOBsiYB~FO`S{Ez)ogU`}iZ?3IYr#$LOfG5V4t9f#70Kb=3Je+@exI)jr&j~V zz$3|p#Pa^R#8R%M&5jlG4~ngML(UwoPja{^-Th0XN5RA{6j;<~ipks=zta!xPjMnQw0=as*;S3{dn*S-Hw zOzH?(oUth0XO@@YjQDYm%OxU(uqj| zOvjZm%W-oMp`q!=3*Hh7Vge)AFRZZG6BSi_=m09M>0Fk5&z-X?Yfu%q!AtD{1k5VE zzKb~qpw-dj_UcVqk{Pu|qjsa|?E@bY{+n;<@#LyTB$NR{U+Wpmm86u3Eay5q9Px-w zl3)S*)@N&ZeX;rjYraIA%5fZ_fMA1ht|*d#kHah=mfFk&?i|>BN+4=rF6f(dRggR` z0~h^EgI)k7kB5+DsM)8fWb38YGH3gtR0(VPv@qr7PZT*NORw8sHo^JJ)5`+m`7GxF zuQhkm+xR9jc>7K&mMYUT56JMA>x;@?q1!)U0$C(+VRdnY#q0Ro1%m-5lqr0kw=6T!&eFUvza(6*U z-cbx81>bJrRIge0jIM)kZ|)0DjwSUllgns^J*S!*kX^Hu4}03k(Lf4&tGfJ$NZZ69^T)UyUFp)FCF=ADOT~{0+*aCWqO?(A&;+U4mh2Qzo~ z|DH4Xy7BS=qzm~LQVl$hOrfJd^Bf1W{VB;kv?Mf`f6Ca%04<4n0EO))q!c8O1`kH7~42*?Eb+4!d}ufJ|YpFp5IYRF6ZBT$b5 zYVlbU3Dn}FCFmc45o~|5Zu(QkM+-PV0@JvlAV(uIWKCyMsMY^2x%ZccT*_xqSmeVK zDERNnDKId!e<{cw92_MF1+`_c*#2AFP^o;#dzud9Wkc{$qV?yHM6{rj4|GuLk2KjI zfd?=Clo=w?9tp;U!vBcMLcquFXQ2Zk(4q&uZ3a;PewO*KlI&l@;NTxRpxI^y1Za=Q zeKRq_o#d@9h_r3o$hy!0iL{gqT0t*eHAV(`B6#Q4i@Q;YvUltr(Yytt9K?SX}Q1Fpv z++PZ^2L~sZf!12Zq0oON+5U3iV7gb(Y}3rhLBO^@mGwx-|I$Anq;x2()p6TFrH!{Kx7e!GK3_nJWY)2W7PrL(2*cPKCHi+5dADe=pEfyBrky zH(2t&|0*9J(6J{I$g~6S=b}1y{BB3KgFvwz3ecpF;y@l1q%8zmYRVw`e@^k=sr>)V z$2J@S(}JQqaiLbBW2%t4V20Giw}cH65@!zMf(13;NT1p zY|kI<9^39eF7~LCL^gWD!u8LaE@2m2( z2ISBq4^8^d+Xkdf<$jb0Y#lTyQ#H;%z65>jgt*5jDr6kx5v*4ah5c$sK@vTGy!4-2 hDFmc?1Rphm4&T#&I(ktsIU$XBA2tk(YtwJye*lVjOqc)w delta 42558 zcmZ6yQ*opfy5&W^24$F}c(&V9J!+^6}l)?!{J9F)qy5NN6VI3T1iJBq$!YTQ8YkOsZ4a&$=X< zP!Q1+H=H_5vgzShBO9O7?#$LkFOg7xAe8c$8JZ2cyf!tqw%kE^l!tw3L)K7}r&HGY zkOP_vH)I>awu!Xh1E|s!>qy?=FMwDvW*hjh>JHq<#jc9j1Xs_2+DKi5!Znfe+1VF& zrZNGOsrDX`xGetFWCLC4y}0=9UE!u00Sa~fqy{xkjg|giMAtLtJLv(k-dVkAvUu;_vky+cT#t-^7UnDG$6Pep7?F9 zQ+EnOAanf=QOrb<^mixGv$!aZ7+l2wL@SPt#MzQPmDH`LaZ5U?r2=M|a2e0J_lG+=aWru-= z<7Fz<)W;CdvRALjgUQ$t0HD;e!i-!rtQ9dsgN7$cntOB@S1eF6W;na=I?XLUisOBn z{*);`vrofUT&~4!5*RZy^bBKKFd0Q6XXlFTpAgfcTLQk`A<7wye{bfRR*X9h5c|x_ z;~ovkYl%}U?b{!MnNd2DoEu}4K30eq95D;ZJ5D>QiEhTw;X$l0p;4ruDUsnx!p5i*5yBuac`{Tsz%xQWU(~~&2*6!;KB!V-`tECXb1j<@B_t4>-17mXJz1?ccp@X!%wgQuI* z4_mVm%o1uM&^)ePlwV(BP)BocXn4TBCTlVw$fqt0Ix@$Ucif6TDTSOxKfN2s_ zH+n0kZ&q*EA^3#dirbZcwas>>$Q-LEMPy~j0Qr>L{XLx|0c>ht8~$FQPXq)Wel|q5 zYaa?LlN;5(1VT|IgCiV$K_9J<*xl_B&7zt)S1!x=!QX6(_;Meb{|KMJ3Z-AlAe z3=oH5C7tv00DF$5P#CeYA{hCqx9s^a`#KQw2R^|oF;8lR1B5EKpoZ}K6c9{D7!V(e z*aHv{^@kj)2a1<%xim&s>?9D;d_7wDt?$LC;}`xeDtc;5V7gB3;wuEEanCxQ?%C}s zH$D;sberS1FxkBaB7tl{?dUymUZ5S7`v&VZH=y;xfa*T1fe%>0L|TrQ$3B_oPHlt&wU>T-QLHy-SMf_0Nkp%s0lVwNveUY2&33 z|NLWVpo?{bR(mt!Yf5vIEzvCRCh|sJMc<`zvm2y+8a&9rcdJe?u zShV#zSG0GX*q9%Y(#(;rg6buEbjf(A&C=R7T~Tu$elE5q=`500tT z6D%y!7>!eGUoamQSu!gZd^=;(WZxG0zBl^dZMLNM-L={6TWOQ$~17a7vB}T@>D8sPoOpnX9(11#_;8WfAAXAzw zP3}D5D;6l%QP#8QnXM^Xbw#4YDACBrylW&Zoq{koib2w+&EnoBdAH_6M0b0_Z{yGs zpGHOgs*(w>r$hrn9F27JO-fS zG*`u+>`scwr&inIQ&r8Odtl&rSBoFS+2od#Ok#xJNdLEUG&3PqfWrKb3jvUn*ZfP_ zLzVNV4aSRYr4h zyiz>GLSmTRGc$Uj(Nb#2S9!|~Q^tb3Xx0H(WaeaC4S&a<5i?qQ16bW@k7<~oy0+9Fv_FpU_rskExLZgo(~f&!TO6m=PEZ?%(RS% zGYZh#*12B5kB3XHp|Q(c^j6me(^mWA(RMB-Q7j*@kip-r`!5cF4DoS!1z2KkMui-r`eeKrB0^;UJ^ zK2$TAkb!`cmA6i2Z_Dm6HDonzVE!NdntTkZiJXNdm7ArlBIm}p$#`R4TUcqsrfM6D zl+Ko<$JRfbX&3^;rsNJ@*A7N7UF8OjZp7iTU1uk&h-e7{xmV$F1pmrIG6JHb@@OS@ ze^|>q9KkggaHGms)p3T&H&`RUpI6E8_E*L6msgQd2Lho(Du}%K??fDFiyyiLxyHZF zEHQa=2d+t@RhO_l&gS>9o(_?+M$OB&<7%FoyeS9T%U3X3Tyjzvec7%frVYDvtn zoGENqM@%bZqJS*lNqgJsf2@6_NqNXI?g!01W%%(=MP4oJ??t5tBO|T34Tr^&hk*>?xk{x_-qFq)9q)WvMW5GENXd}={|NC(SKwfwOMcsd@h(a* z;L^Puh$Z$2ttxXhN541D@%#4=?6o#Q3gHCbaa6KB;^&@TPnHD#Y_dKE7Ho(5C!{ZO zHbMqiBIH!|cG}1h>YbVD5h@7$S^uuI83$umazAN&AkjsC{1EK}M68$XGb&CRMA8G4 zCJLnN27@~Jt;+(ypYGaR?g?b%PPRSlo z3+n}a&&^G!_&eLFq6Zz;FrN4)w$#qX-tt! zxBp{hoOf0`ET)ml?^B@TZ8bmpvU_D}Lq01N_c$YMR&bdDKPUJN({K@u!Nu zk$olXo9A|vy8BHXIKk%>(?PaB5ZYLr%9;(Z$k}W%vkPYwo;&NZjZZ_LR%Q4l#W)*l zPT3^)(7cgVc30)^Y2)cg$>k#>Y=7A0DkhVG-kX_E0p)ww6Vz)K?zj={$V5#ch0);T zYk7WmxvuZ9)>mAGE_>cnmVG3d#lX*%ozz#ErtXU3BfrG)5fZd2z84a-kf}o4{DlU{ zu2bYawphtkOP8Y^x4MOkuDaVcg5UUqTXdbpJY%{Em)2)PYiC!Tm9KfZg2JnjMXvzs zPTa`GF12N(ewF7>PZ8p8@a)Tb#Zh~j3A>el<2PSPwI{*t)&@M45H8Fa2m&phjxJNA zl{aq@cI-9DvB7qSU&@L446^(_jcx-_8q?lgUj_uZL7E%7&j342Yhe>3hy4$7xqO(3 zmG{4UEmoRRi?bJ_t1aNT|KaGIN1NF`OY-i$W?l|Y<8i{Z9G_Q$1jqYS4%&GJNEkfO zL~A=jPt{hX$Kly}?r3^UTkw{4gTshNCX#J_{`G5ldY$lCQJkOu>&e5x#_I$~x8>b! zqdc)o^+||RkxkAC%HP?jGzvmKN#&fwjqfbJCyfe-h=ufth&9nqbRkTLdQFO@52Qa* z^OhYW%}>08kp~($KGGjic^izeYMyFxm0)|csETk{e5N`Ch8Lib+2Hi=Z!iba9BJ{t za~NXX-a=vVe7wd*?oDNmp``-4)HL7G%j$aLO>N)0x=dqU^J4doklbG$vfN0&sgIOi zn`0GvNRF6Z{{$g=$z=_dNwu0Pwy%(u1g0Q_-(ix~L33jlg8wpjTKR8!X>HoPZSb3M zmJRE0T))#mb<|&4!{@t}u}TckibBEN9^EtF9!`(#XCr&z*7tLZOT7bd?vl8?jwW1} zi#<6u6lRfLPrpJ@S!4f@J2bI|ACmWWSdJ2W;p}wC7$4q9GzKl*JT#$pi5oSuBXMeU zs%SYAL)@kTpjC&-zO=+IGfcEVBB#ox=2K?v!RCp~B)Qn&^ir-(2`yiaXc+id?`tCcgx z3!p)M4e1i}h%spTBtGKnkWRO!Yy53~&d{>ldr78Un9z)@Xo{rR znUCnV+DApmLJtW@qA<#z|4}7Rde@s&hp5#Ey`TK19ZH!DR>w#xTEHBU2t==D@hDFAkvYaqu&aaIlZ4Y|-lww=2cqkH9}J4)X-;3>>lBmi6-X0tqQ>$0h

LrDE&D=*==+Ra$R9W)l1&p6})cwbu;z6RCh*R3A{Z+-+e;F(gCx zMYM-kcd!6O`+w~V-Pn9GJiH!zPyV0+k9Ot`k(&?JB}=U--oDa+UUZ8Q3EXn4oyYEJFBweaH>Yu{e~j_IJAcVok44*f%ec6I$5lkvVQG^djPkfRal?P z=WG3fpe1=qG)l<#RwU(~__QT0=lfL-Q$9DkR{n+ zbX`H0OJR%M0Z3+Z(c%I;S}T9a<9z(F)bx{%VUZN-yUDrnKKbc=3HVqc>h({H4KTeT%*SsGs(S zw50Dst8#p`v;UCIB3WDkB%*)d781<=We=4yLv@f35Xv`jN$0Fn#R*NoLdkXlQh&o{ zKvbpdLN`R`XH1T{;xHiH!`3Nxs2z~Scf}O%N^@;W&&$wXQ6@cut+vuezj_KuPv3i{f z*`8s1A?}%hk>Pw6N0wpj)IA&k>2Y(X(DupTtA!jda389hWnMH`9|cw`W^w*5pg= z8BB;4O>G1Q+SwqA|8tEWJm$WfUo0D=Z5cIvGa*nOVV|JlV6m z#!N>R2Wn4aasU!yEwEzD>@3wqbI)qz@%qk1APpCYx>>4xN-bIR(*-3w*FP?e{cKpi zJJyc*GSPQt&(T+R+zL9v2@3>!&_>ia%mNB*GQIaG2vwiv@76LmH!jtia9-I zip;|8c$QuJfaHKCG_y?FdNF0*UNUujM`DCn}%-|L&x$koy- zrX^J?FtMjqM?eDw^Hq&6L+|@tWvkl$5JkYQu-*n}ZcLm}onT_Iap==| ze_ZacAUDA1RT&PkrQ1f+PVj&?Wtyv@4;^xM{e^fS;rD{>x)swLocV{9Is!{(t0gtX zYiM!zkB+8*zPEql<8EgD04Nf38a8%nJZ=iitfJn#su?|{k$6W}s^g2&*^!OjBhF%N zN0MyR?5~i~<#=d-9R0Gzi60ORt*cB5>0EaiVK$Y3W9e2-f$$=r9AnLXYhyo-WJ;Kd zC%h%`qy}X=mH0*m@4s1?>fpW%&s<_YIRY9Cs)>6e&m+Rkl=xS=Ca=xK;wOU$R`bU! z_bI)bkngOwz8vvS6}O45m~p5+*b#13i6rP5imo4SPP_;}gpH$SauQfcxI{5jOPW3; zNs;CW#QZyog*PzxYC~;x2BN!iXIiy^hGE+0e%SNtcnw3svyaW+cc`tr`PMP1!fBqll@uz6+D&ui} z@cF$98;<}WapGt%e~HE^(95MtzlQ=gH;)>5QzRQO^NZw}@Mch7{t|B)e{0Rl(~}he zvmP}|KVa#s#JpN9dlqOv*|WEvLwX-pe}& ze>Y(1c^c{rwCakBAp)O}V`;VH8dPES(k{b+HXsBbSOvs7g-8p`%2zuhdsIn-=Pdz7 zoQ23z?!kC+c{C@A?a+?W6afR!)jFh(4X@QYjNSui_eMW7{u*r8Xc`&C_=5ZnUG7#7 z}a5GBmc6 zB&`8ZlsC=-#~sv6+XS|S(ncZ(5w2u_s9Q`OOlhkL=&&p3bP$rRp`2*%21f;qNZ*erz@rX?&i(-Ix~smD*8ViRed zVU5}jvXCXQ^hCZBlJ{S%B!Xh>7)ws!3lRXgl1>Ep+MTrCzzfi6E7bckIKsx&2~SkQ zN@xRgqUEm+A9SV;RCdBO;Vjs4XI5r!Lv1|5H1hXl(v*JxQvHz$o3HdI+KYqN?{PmQ zH6kDHw4)v8W^|=2Wr4F#XQ-t;KbA4S4ObTv<@fm+Y8yhWYNFU$?ej=8x4q_AYODl+ zwJam3kA8WQm)p^RjKWVJYa=H~lFLWLMcuY!Ar=hVi8UmS5q`7~&h4Myf*4n3Ry~8m zR!`BiVJQCbPoJ;iAFZ^T;%R@b_Rrt@a}V}eICX=DzKmYW-4I_I>Jun^@bc&Lm&n}k z=O9E56HNlG5b5<>qOyMPIs+8!xupRh5dMTIN=lF(QFO{D%~0)CC5`Ba4~-Lnam|TyZiXx%?ezfCw}&cguc$->;-OS;;-`dH&@{UL-!R)Y7S0@ zK;BR%a;l}O9G};ok`wV`QklF9ZOwK4MMP7hXA73wcsSi6gptZX2Dj-8MqdMfn{b3X zPkx*8T{EBWpvaJQk?F~?k+hDX_9)6P$!eke78JN|7%E`i_}^r{sk^d~>}UgKyXagG z!vUJ3p6daKdRQcR3O%Uk=+m_d{mk^KKa%VohOi29yEGP()lMWEzbi507!Ui2sq66C zcVAJUXtabI3>i!);rE3L2i11~+UoOZaT3>Siqr$6=_Ll=o!d*rrjw=SB(n`kR&W=S z?o+ErZ(2(z%##KRUm-L;$}JddZ^>2L%~GQEe)JY&z_#mi?1tH}a}AA99B}z5EqE2( z_+f~btIZy1>K?u|jQ*&Yq-s~WEU&y7$Ob|JcS0vWN7xX#{2 zqphaZYY455BvKoZ{L~1kW+9(r2558#w*~%)*x#gSM@jt^)JPC3QU$!sE_od|&VWKbbVEwY09V;i$LL6ui4+o)3A=@QpDcPE`sm`Nir|;)bwEifAM3QT3?2 z3Tdtt;~lDXe-#rxc;1iK%~VmZzFwnK68KNo037p4Kz^lQX$u>O0E_q#t;(9tW7xNB zz4}Do4HDZTgD5cZ_K*YL>T=%(hr-sDB-KoW?$U{J_9Qk`ly#gaWD4wpGPl#PCo5{< zz{f3JOPdYl2e?vGWx;Jux#{j6Yc2OTW*yA?Sv84Lp1*KDdMA~x>D7iM=krX-6)+BP zfHXBabK4S)<%5alwX%XVD?EiocJo`4FaBOE^eK`zu*bj&1B5dnVI;A7jn@xiNkQKc z7W$=#1~jLV*vUR=Sj(vJ9KC-NeA~~cE6fJPT|T-vRW(n2q8=zp7pF1k*(DnFrcapO zcS~8vD=`@YOpDp5Q`Uc7*maMk1CXz%0l6N~oPwfP?y0^sht?~XuJg7WmlLydrz*mQ z;8ZV5W{=#b0wVXNhch!NnKVf+hGSs;35Ftj0!nsdZY_?`h|osGwWq5{?FKssPnWkV zueLo(Y8|y>?Pb$M_w>xze`qPo~9M3in48qIvxfMLWgUR>VOx=PMa#Ks4?d!0?gsg;H>U{*u&v|)bI!Tul_IX#L@E09R#|9zS%63Z`#1Uz95%-@w_ zG9I6H9@FSJ+KNoOAr&2Il%XAd^8_DOV}28xfG;R6fE>8>G-}F%N_62Bc$<*3EsHnpET)|01xh{2Oh9lCys)4Q{o}%3}A6~Y43-b zcw}0|18FC3-{?!xjj!!pLW&4>ZWFC;vRD5oExlVp>DAh`f#|yd{%#!9-U&(w&buXe zlPfno2`fe-Fhl?PfL*8ueOnRs2Xo$oM{hs$gPX~41*RiCz<$Ag(T*@m+502%32-+j z2`Xg}BzalRwxTimuA{ucF;}9U`Xh16fa~X0BtbW=?l9IN%HHFq{k1T5j!cR`?A8`? zjk1+U(R|~EHLyGb5E?(?J)r*@NRWVC{df9`-&--ghkrk{9F+)&SA<Ye&Qyjvhx`*aWHw{R{rd z89)c(%(v}Mb?FA0&I1;c*$^D|{cC-nq$@;R5KdOh_@vVw09jq($IJ$|l`rg*U4oU@ z;>Th)9`mS5S!)Qs4&h0+Fh};~M?75e2)lnXn;uD6bA*#4Q=??8NDNdyC}V;Q*HfNt znLxa$sO)K-RRdcYx2*dDF~*icE|^-;_%pP>wr91s!6CDsJiTO+gaj6qe|g{L3F{mJp(J9AJc|M;F)t^3)6 zm-k)69jJxB@WyMagTm{B^7DN9`740zXh`%URi9B-;I#INEOV0Uo9hfI24-^9#z;zf z<~ynRCn4Bg#+*WWW=~4hl1lLQ=I8&%Vn}V{jtr$_(Tf5s)G2~=P*a3+K}Ibbmr{X{ z)C!^-2r|ra1VD*(EHY9#^>i3_xr~-Cgn%nKqa3VZt-kV`FRXy`errChra`>7^N8-5 znLHk=f4fOPpC|1ZAiDzzte_oKN4VwWJxnILl6aQpGp34T{Ftu|)Wg3!Vkzh!4*H0Y zI{p?B(V+rRQ6bIQO*DqGV?FGpUk4J%y&fWwa{Rq7$0Jw;hsIF_@5GL$8E~``lM^xb za~9xd^U?QVq*fs!)hx4NtfTE9_2#VFGh3@2&&Xj$W>c%{|D=z-NEBJ$So8c!>aeje z#gS9EpnqOgeRqB(RE1;&~t<|@+Ly3WY-IbMe6lAf($y=_j_b7m#ULdxP0@kMsh z?vE#ckA-zEKd)Y)K=uY+M9$Ww(@m}-Y^Pex`U6&!8=qlA7s zLCgG#971EFG+5Dq{#V$mIHyi!xmH)5i8UTD$Lkj_Awu{OnqQG`tzn@wsD=-1Er_cmBgjNTS02pRQ|I7@(V|^vV%LNuL$vZQTZ^+7MJW!2i*AX%ou1D%$?_f` z<0669JN|~Jg$VrdXTCzh=`XxC4r#qZU9ECa&T!KGg`kk3PSsnnMb8+kV)~UvC*w*N z!|>&Z$(-fJRD{O8a=?&J+Du@f?o6Stl&nA@`dO}84@&BahsU@ntw1+iRnI6v+Oq?O zi2)5%f!VWq-pL+LphSmc-s|5T%?tdN~^WQN1;Zmpo5=;pwmMF7$9xwjn9XnME4c{?uau=h;77oV>|u zP66=ybwj4sgvk9in=wvj}cg`hwHWy@g{6d32sFLa@fhmwpKd2qV=nz}= z*Ebn^eAIa2Z_{7(5Tik=17R#q3wRyUs|qqoblGSSEwOJQg2Cj$rB;d0wRlYUE%=J< zX#Z%#E{X%VAfslX8#5^gFTle9!?|un=qC3X;9!W42u(z!6N3CB%(Wf!DB@Aw(r!qO zz zN~6K$JI-48=BQq-?XL{5K~P>m2-E4;Qz!N3{f!Y-M>4!V1B#&J8cLV`4|lCFR2W1O z3`(BK)SH5C0xF53NAgfO^fh+U-683U89thOU6}nYMQy}4$j*RY3o1{~QYt-f)ILQ2 z(MadV|8KWvske0}@*e`(iU|Tj{l6iZem4dnP0Pj@-vTF)a>Jc#%SM(;TtYBh9Um$Y3J7x-;$nnDZQSal(k)Htdfy6xuFXf+pQ~m z=n6@r0Of5b_w`3jzd9w@%)-XOzorqQvB2A8ZE9uJ+mPY5%hK6tLJ+7huCg=Qj_X4oti?)U<)IqBoeTa5=VPUZS$IRh6~drIXk>sTn`is5IoR`hZ4EIqNn| zPGCPX=~Y4l^=-l&Y&iExK{vHfLsG*YEJwq;rI6c>(pvMJfM z6=Up>b?HG0c5StK5Ec03j0Jn;A)D$d+6J4;s$zZO4uUl75bcRcCS_-v?jmSDzHBop zUVdiCdzQ;o9$N`efWdu58BAJh2Omlryk&xks2vRkEdsrlH1a1|BIydce2?sMg^w2d zeo8`{^g?n(NOrsA$Yres5i)qK(vWgA8ubNc6_IZj+m>RQ8R!iAKRgNrSg3+bsIf0P z4CA=%b;-TeYgW%l)}$5z5Pd0WsX#BAw?}`q+Krl zvL^c#snOv1kCEZ>#k?E@gT>=D>ovnxjDv;*>R!>gY!toKUK&Z+q2H@u%%9U+XF)T( zCEp@kj20hGiI}01Zfp4#ABQ@gY}U@1Oi%T_aepVPVk`>;MQJ&>Lnzw7NTR4AqUL2q z)?WnzS;1iPfrOcMPFnG+M)Ot;*NVO1dI! z;3W-4KXSCuRFQ99k^-{jj>T6R{%6g14TDoSwH>~2$g?S5=uXy>HP$ex( zwBC=QZ$n#~mRTpoRibbS=Q!1*8qHSjZh9@9+w1oN^T2sci#>hAmOJv%gNjRV}*g0(Yc!Ygx)K4xk*kI z_N7|G*(J>Iyq|6Ruzp(}kM7B-+9;Q3~L4jwR}EDkDax^fixQIcwY24V+UY z;BdO5Q8?e3jY}YSM^#4pmQU#x~)GF{T{3vg?%&&`Xf?TjeO!e(QY{R zl-BojrjPWos-REhf3XrdVKMC^eyw*hO}F-TPx;Ra#X}Hko?`r=19t$_^%vbqVeEX7 zEk9;ezJxI%$5FHP0&mQ|iPW`XJlq7>h%?7{86y_ERK$!p+#e7$hz-aPL=ApU!0&}T zZNZ4mJ8AA)WXyQ2anu*FywPGQai^N%E?#vv`-()wE}wWMhix|n%R1c&wh4_tHB?dv zUe{5x#qq^hYL7Hh4dkny;7QsSD9huAQVR>RCD#K8wGJ;OES^LOR*mkE)44Y0o&J^$ zrkHeA&eh#(f3Is@pB3F+6un-G0du=J*Soe}H}Y0avn1>e(A@b2$G*F?`Ss8<#cxS| zHg~!{$Ik8n*CRCe^HLzQ+Ocp8L2x+xG0wQFDVAV^dlNefr=t|1K5TWPWq)r&EJgEP6t;I)z0$$d3yeZLfN3E;Gl~i)3rcyJ75e z4ZAfLe#lxOA-j_$y~1xMm`>vJX6D}M#6mW zAIaTxxsj*?+_)2bTm$J{0lzqA_J<}U@J*ARcF#?%T8qqE7xf$tnSaYso3cshZ|-4s z){H5*il$*Bi62Jc*y9{k7db%YAr=0CHH&Cy*mJ4yljpa?sIWG|p#KYZ=8Sm(1FwKV z*B-(5^+-RO-Hg4?Pv?N7wZ=B0#pi*u|E60WD)BBr4+_&a5I8Vg2!Lghm^;IeU>~~2 zd|BuS)|P(NoGr4-1u@#)ll80Lce)@6h5CzUHK(~)$;+PHkmTY(4{ky+2wqfcVSYi) zI~2)zza>5}I#3DrR4sZ{=X$TCD+xBcBS-UKd3BVKz*HZXJ6#{j`gc{s$wzGS2+nK5 zO=Hl*yKj8&;BE~X0N{Ce68x79`T*B)W13vP2$UyvsC#z61HD_tkpHmS(Mu0tqD<#9 zg)vm67N&`KYl_(q8_{yHWFmD)W9v>@Uik~h!fNIoKnM~g^uy?lXzwnI20f;W!I|cY zh4@NKudR$dGM%Ta#q%|aL5?jKR8}ORJ#YnfS5g^8Xzhs2<8umdOc4NDV z)oLWz3Gv;YO5D|);EJZD=3h~xZq{<8v_U2L2{QYdg>VY*(JPv?i4Nc}RKrK&UOC)> zwP4)6c5AFEY@A%GT;MGNJT3j-P}$GRIFRB$I4cq~#tvM7u4M}J0df9cqQTX`wYNDUUx?MU+`hNS`dkK8_xfztY z{tqJeTr4+*Qv7o|@?h}*y3wON8gpyP;~Nz{eFsqG@zp_CGpswr>=7NL!|0hx(3*Z) zt6aF%g=n9eyijM~f1- zqVCv9@T!JBG@#v$dzo1Dvv?5P=-Ep^dUcT;CFoF_qj_Y+AR38K|I1QR9E9|z1FsoY zuo$pUh_U5TAKlzBpO7%cM)YuW1S?1qQIMMRGCd1LUh?tvnri-NBSiS$*jaUNv*Ev* zkYoPCBP3XV2QX2)JGaOQd>XuZO5_veF587WQtw!^;&+}e>~o9@k0=t zp1&KgcsKZ4stz~q8UC3NhvFe9QaQK`3*6RyC37Kmk8z$O!SEZ_Z%Q%5zpnaoUT zL}TawN?EL}VbLVqx1350!km4MOC#pp3rYL0bsme=*zWxnlfVXp1gZd`CY;> zebvK3whE9&XrOLA4jtWmNGVJ~(@TTGNG4MwbCw?tH^O9FUJd$X08N;+V+6@BJU6ihbnaraq55_&mGu%ve6tvkoRz&t}?2 zc|HnD(8iu$qm=M5 zP~G5Y5ofV>jqR$lq1wQst#?tV!I;HGG}SNcY$7(GVVNqdqiI|i;cd*9AOjw=u8lz2 z09i}AZH6NASugLaH&M^I*V1%mc`?&8h>@#12F(%bS=~H+Ht|K51^LIygA_%(J@Vp{@vSx2 zE`f`f2Y`@nXj#^t=Vym-o!@O6;})LpaL=hHPGjW4aJnm zlbX}<6Wj{*2@vvUWl`8DUw zHbk?Uo{I8Hkk3&h$yN*_9K^9jpcn_R89f`WJ?{3YO)mqm543_rJ~`Asv8ATHeTn*^ z;BjkEROHD-su5kg5!cklOWkA>CDVvt0TKdkrFEuzf-#mub~w2bbRQgId3q1p(?4DR z>99}DyxDMoHJ@5YUpBqS&#Ma#ZUfq%1~|KhtzcYt9n7NlMMyN9Xh&f-#~=wrornYl z>Q7ag_X}noSugj6iDqcoX^6R;!7rv}0Cq8^YOi;*c znL4_=3=6Yu)f(jsSA7N^p@0RwErFOECY2n0u|$_8lp0~q+a9VlQt17EfmuiL&Nr*) zEQRN1G+k3S0_<6EAg>UTZh#)4fsZ((qtt($HK5525-YzfHU<>y#AUM9Iw>YtF5ty@ z++4%ECTQBS;lUndrVi&)@Mr>HYfP@MEZ!8%8e~#uFuMX~Z3>; z4#4uJ3nqgueUU@q#!yw1auw-Ij(Gn79Idg*NU*fc;86Dnvqp*dGtf z$3I*g1_bgCeU;Gj%*2f;2yr!n@S?nk-T#$_K*hclv$`tzh5vhS`ga<$Nuy|T#W zpSu|BHl{!|f(^<}^lgt$VY0`e@087oLvZI?7NGW}f?$W$&)#fkB2}@pqB$;hD({zo zMz^l9%W@N1QfTcc0uD}FtGViC5+~cSb@-q!o`;&(f6#lFEREucx_$aySDluyP2=I; z$;(Q|spm)I{+906z~@@psk&#|wz-E~Ws_=9WaTY+wM~ZbbXl2XdWOMDPlO~b=-6DO zaKJgN8KyI23L47O2#>RcZ|wlTRuZU{_$I!MUA#r}d!Z#!O4}d7=~UKy+tzS}Vc;R? zAu)?vjk!9a%x_oWnGiMO;#9bQrh4gQ4aQv&WRpD^91xUv6>7UP^lb%^ljJAt$HQ*o zoHrRoSy2Za&Fluf<&LYRWDgd)ZLFWHZC=3czK+z;q($Ti7CZMN$*W91Z zSI!39GUsj_wwhE&%4cb-f#{Fho@lJwV%Za?+GveRt;GQa@IM;thZLfjUBS_6>*Nj2 z$re}`by@4_t8pDI8a=UE>OIkG)N%-`EEx3$=#NW!j%H#}wq5FX&O6dO&SML~{X%lsgX5wV-*v7=RC$??dw(f~- zb7I@h#I|kQPQE>efuItCuy}Nhs-fOL$hksX)Eq#k^(l%IYzGL>j+m{-C z*OIfVCb}|QMMtMnugn?rf~>B{WYBLlYYfce2S?xVCfLQPFFYJpEo1hB6OnWMyVk6J zi1a(ID1g6Yf_XBIy#L5URgeo0d(JVUU+m7+Mjk!6Btxr91#_*SUWp*Rm1mAv4bJ%3 zjYf^6hDZ54Jq=QLp6Wr_idRdc%~^fyA#u!!?Ph)TZ(U@=FK!mIKcM*ohD;(DdzQd9 z?EGf$12hLCCe;InDgsX0A)Q~8V(`d})G}$QdiJx z^DvjN9b!ayUKKo9Ex$#Pc2-ljrSe%!u$SJANX-%a#iaWw^UZS{it>0VZ79iXqxIv2 z&^Yoww5a~DTZI^8u0B~Sk}|jZ{R99`Z{U?tfAT0#Br3K*H7K|92q1>1UEH$Zq?vwH zPG|1!ak1HcZrL%4DP>__NbEl}OGn6cdBru18KCC|^Xm29%U1^?18y%6=vr&`oumZwee6+$K>9oyur1bS1Z~kE?w`i;8ik4pJr!g3 zLBfi1ko97<#wL(G3N(a%`nl~4X1FRwRVLQ}?@*=xwbY-Yy<%3j=r4ogp|^ zU_6uLur9QtHKSG;X_7Dwb2jjPj^PoC-0c^SPkKTmxnhu>C5ZC-bOOjBR@yE((jHCQ z;#c|nb4?h8)(~HFK+qavtyaj{s2Gz!gCknUFjT$;gzE(C9d*U7i1;pc0hWjV7yTzm zCT(E8JjvauY5h>T{T*fbkA3_(|6~5e$rf=})V^uDB5ul*p;^puwTiyTLZD5D~ zd9V8c0-_2A0>bhC@yFm22xZ8DR*jYm?jE6QtChPMB8Vc0{x zUE(DtIIa>#m20Jz%i#zJQ1QkWr=Tx=HxmQH+D7*sp19&(l^J3Y7w>|_rvg>Ut}t|r z5*aMKpse-E3$#jjeu0Z=kUjWPsi%uIg=lJd3HRFl)T(>#HeOOHt>jzRNjq{cC)?X6 zxjGH*km~&rhX9F@6N&Ky2ucHm*=G(ykk6|Mtu^&DY+I3}Y+U*5!p6L zV`^Gp8g``RE*c4yb}18%5#tDfwQ&P34wg&)1NNLxWvbn-7s+c7=WL9Z;}m+gX79Fb zj+}U^2l6>D_!;>>_|0Xf#Qmlxf=ky_ym}!@oEZzGUfvUMNdwzGBHN`8R}X zbqjf=4hx!%)$loe3V}%4^Bni+GM#25R)FIeVk2 z9bD%{SMQt=Jp-9Lk=j0kgY8b;VYX&(n7S$rlOHVXW>t+S_{t>!4ja52s4WeAr zAUoxMCHF>zVK87&4#WRY963P!(RCg>X3Zs3gT}Q6)*v@~>;1a**Z=7`2o1=9UG3RHX7|ym_LqdOK~kwJI1K&Ua#;FD zr3$E!Q1`|crB~HpEGws4wK*Fz)X7`DEIFvpb83>Xe(ktFcNuzY>kR9>Z297wW;j3s zmMjgI2HjP#qY+>}BNEJ9|6oDvAX`<|={ys_y;Pglh|%Is%&5~iO0%bdsUCg&77$5i zsV{QbOmUajTdm)^BH3uXYbvxltGbR37pd3YmN>OI7SDKm;f)-mGT0e&mI?7%T)My+ zs@V+c9r)SWvG>|}H69#biJ*jdYmNU2gzWP~ITc^u0_=qy$`od6E@U4?5o$hE<>?80oOKAltis7i^a#OSb+&4`up!k$aQVIgNGD|h1=v~}7Ramp zEdtWz@!B(G8ZL;TRi=nD=kWtNhu0pyV9RgF8?zOlx$7F?qsy+q_UiC@*x_hvuFW zfi|c2eUzWlkbvtUUmZBN$o4{D6*YQW!1ZrowOrFgG;=q^(xHe~-X{SezD~fiL!?W) zB`!n)$LPlF5w6?VhfN>aFTs-a^zpBLniMcOWp z_z29ToGrN0rvyclLVeLV#mKZg#9I80iZFAz`51%c8!VUU?hdTs2q7czT)ON=zk<3- zwdOZ|MGiOC>9L*OT)gIf4nD#_BP{+X4?avs2-wE!P!I<*gRw&-@smo4>~^;MPYq)hc*O z)bwSiGWoE(bY?#cvm+?^M4jO5Ze=+i*UZq1gDjx7^A;)mA|b}ULXUS|Jx}Ki1S}0* z6Jj=PTb>IgJY+V_o+op%#DO(yCU`FG@fsRxQScQW(r$kH@(!#B%RL%#JU<}pQa`>u z?U1*Kt#_Y9xc*EtOhu0)aO#$PDz)Jy6FA5PJ%7Y$mFok0#PM@>47}@~MrhZdg-_^p zbnG$&!6t}Ag2z6?A$tgjix7Xi=msJ${0H!VyVf5?T>A>&DIB>3FI8TAcuEW$K$(R@m!J?;_e?`3nuz8^LD;5Wpe@ z!}o9+4A_FU&qC&WpdgjS`1O_P$DNh)aW+WE!JSnB`ZLi{Erp7|AZHKbbe}V4X#172 z)96z|ty&Aq$hBc>xi6npL|UIw8@Iti>|$w>?!dn89o{T<52UKxpL`Kdm-=&EkT8BA^z^(nSB)Z zC|RYJ$hTwHLr*%>Pp}=sGgV8*iZ%Pn5CAaCUS01F0G7gdE>B4D3_^5n2e$?X#W)A(&1E!gVmS&7vut+BFmQ;KCx`>v5!w_eDdb(tHOfdPY8kEWGJg#-YN~KW)dJ*)6d9Dt z4i)X*2_22d9{@5=1RVVpI%5p&FS_#H9RSpQ#uz+JVC|Ps@cYOP$Ar!yQ)>T~VnNh# z+8FmQ9otM!{ytSS9tvh z%5lk~IR-Hmm!h1*)BkP102iP6Ncv8p7N8bg=eARuAe)`1W2!79KUtfnIL!|+9_<) zdbsOJNRlMz{(4N}u@lZ_Cw7C`xah}K;grh~hd~d=Su={e=jv$`QzvV{e7VUE1ZQq` z8qt-D*Ok-$8rtU@b`^<#uKX3zu6BH6sj(k8{y$OF|Lp@NJ(HLskZ(zjYwD5pda&!8qLtj&DZIx%nIfS}t;3gQx z03^g|;Um()Ff!)U?Q*kQQ8rVt>MMe=rrx&2S z)d1whCRlyIR-QM1*1mk9;%t&yxB1w(!}7|}DucUdzOOI!X|lkXJUgSx3icQOrdwo9 zS!0jJ{X>BwlgwvpRl=cr4|jNcdDjtv#|Ut@mtuh@WS#An8=!EB$<&>l0G;Pk*`KYDDFsU0d6?hctqhPvi$`csCf-6s*tqJDFrrW6tc99+ur~oGI zJcb&4ve25UbNPT_nL;)P7+iw)!C+@GZ1mwNe)wH!v;pcDl6#KWB$T1UFf${V7INYd)66RRP@$;dd`KJQtb&JxcN89{m@IW~~?NbCD+`>Ku`yUuS@HP^|9-=ptU*iN}59Gcw}M=LqY3)aG|{BTB}M{bVYzk-jurY*M9CQ|rrT(4Ks6?S3auEg74EtCRnT%RC6l&`RqrNxcHyH! zERU2djGf(wW1_t_p~Q$xzFJ&g|~xU!$b}%Ff|GHQG~C^vMgP#$OP_3^gVTct|XX z6y?DDRcM;E_sV_CVni$D(Pc**T^Zim`N|^h)rnFe7?Rj;>9u5M8fKo$ky>K!)}80s zo63J&@mYI}n_#(IvSbY5EJCMDK)c|Bugs#K1Db}6kFF0^y5^)Lz*jBw_>mBkb z^smbx)<-Nj%P9PVU)G4i5?c}*0fx3IT{JM!4Ub(g{;WkJTXbI%vyV0}MF72dWUthP zI6EwquTVPD+L7%G%hS)d_}I?%<24MuW`jlob*Rr&Vl+xfiZX~Szn9v$QCxOWXVUrS zG#zHKzsBg0Jzu|^0apVjrS>mw(VHLpjMAGrb>a_maECV_MtuL5*`{O+kBKJa8c2UD znHsvSmIa(i3J42eYA?_FaKX(DOB1JIJv3^gX+MMTCZ-k?hNJh;*gDExeq6$Me7XC8 z*#~DOkQ!zKS-KCmOo$H{wY2ksIzA!l9SKeSqpK!8xu#AwEZm9+=^uSXhT>J+Tue~P zU*}U*EPN&uaT9B+u!2?6>TT~MJ2p35kKDu8<(9K;SDR}BG*&>o-c(4O(wx<@ zlkfSwoR7f!r2W;0xjsY2wU5l_{$Si~^2PS3{i^Hf@7NW{a&-4dLu6&{K?y28uK*>!f|Y1ncGTG`q>@Kc=ygv##lOKd zR=Yn#NX2=O!axOyOC7Z*7D(S?OMK8EU-)x6JE|a_D^6i(lXlb=AMxg7&1)lpRt1JW z!F#hC=>oqjENutL2H~Wto?hP)P05UU+S^mJxJ^0W@RoqB)jy1`Sf%?ekW=lbV!9;h z6Pvv~gsLY~Cxk$A@u>wC+x!$>nfLPBeAK_msuBfL;Z~(Y6Kw7TxWUVspg32Il`wNl zmT~NEDQY<^lq;%DPY2(ENh zS|c4rHw5!Z|HCAii6_q(qETl_vvOq*B5HX97HDgV&@o-LX4a+MVa3^OW+;Ci8rW)L zJt#(^oeB`}&xbF>*n`?;F99uwx`3364(HqNCadFocU`s>vn z00TTAx(BYn*=PrrQy3LtHi~*Z-KEvg-tn()ZlU}#9Cra?GRGELF$prrmN-NoNO#Ep zp?=2S>BrxzF%et#z>;5B6~lFspMiWlK&#m>wp(H?c4r5ZH&636ONB+2qe{qiUZV`%!P zR`b4XN|aW_TYe?KrGo4KCcL-ddMiQW>iBhruRTcGXy*^?we7K3Z>wsf2O(r#P}=GqxsmB+ zStX9f!EOyM1jbq^(ht!mT=1NjXDIFDK1&oNb5);P+RAacY!cSr%b_IJwvIJVFzc&c zFGKHwfpX-N@bpCY_;iT|ApL>U7mi1ae_1newWpvZRqT>bmtVP0(IQQ{Q4bDWDbV?c z(5_mA53Homg~g(qXh-$1rNuI0V8(Wa@T@d>)DjR0lH|kWK7q1eCFX%dMBS12_cqJ0 z?PGy|-NiyZJ>C?v4SPSoc>8BZLG`*rfE~*GiHGV0+&rpDI-i)zR30l!$lMrGWv(Q; zNmeMlN#+-BC4!96HxFL+AOe3#SJN(7*l0&77C3-Xf+A;B?+)1&EH%}gLlAOpF)Oppj0teYCA+W?9#oA-p3Jj_eW0;J$AKuSafQziU2b95QK#|5 zPG!kpQ|FjaT zWKJ_Z-K^;&QC_+8L2CV2XnS%u!YjOI3PdM(`~xLu=0xAkdE}=h>7}2%85U4k{|p^b z`D z8Xw7jUq3};2x5~i_1(3CR7tzv^M-8Tn8kxD;%$T##Sp!Cz(hKXGZV>f&&2@(2*gmk zL2!WkLcj7Xii&@cRdR+;_@cgHXxgsFyvf`9__wZ;I@rkptMGHQ?hSNkWuGui2gSz1 zrW|2GZK4J;BI_xYeCJP0N(?K+lhPtAf+yiH4uobbJFI9xhXUf$ZE!BQGyNFBJqgLl z04~=;Vpzg5#OItDvJTPbnFX>>AlVJ)YaUyq%>4(|^H!yVHy_E@|4^y@@0$D->y0>< zpkXBmyj+}7;KH<1x`q%QXWJYF5uhOE7@(okXA;^SVoKMTZ7jG56GnshVF*Rv=Oz(Fp3M0M1KmR}xnlN`7 zLk+?Bs|0sq!=%0?;S%CS5gwCcO?H~THT}>d;H?)xaE5mp>vc4^@QUU1t?f>Aja`CS zzNtLDwU`rfD@3x#%dQ1*vhi!GrZP{5Zx(l=9uKnBSffpeg>`8GrdGWvD_zm40Zd7) ztSA?;sY(NiXB=n3!Nu&BDrA&U^#c#WGpRk8@LD+^=>^$gwM$z+HPqF-7vs3fCBO>~ zn26A5B*VyBUZl)3RAXtW|4W>0$#8vxZ@xTxwQ8q0)F-uD8f~?ukkEhh$6q=oD~KdD zn_Dve;LC7@dY0-QX3ftf1)OXHD&SZRepVq|UNV(yU+L)@q+Nux)-$Ip?XM_btf?AG z!w7?eU|J;O*2Y$*EVxWQ>fP2zVqJMPpe`+HpQ-V1-r*_f*aGd+b)-?}??-iT!9i$U zrbsc9s6A0E*stFhDRk({(FP?-CnW>zqN$Ctr z84j`dt9CQ7KqYbV3G>cSlp6#<-QVFlJ>U7lKb(=Hh1@M?e~q!8#CNk190-i|GYTW3 z5=aV~kDU>E=Q_!L-Vn30`uJsI8F|VQ{dMlxbqBGAlrr!iPNSEJN0kbQQ4sHtw4xcu zI?Jq=1~Eq_3uO>{Tg8{tx)k ze|Em}C);By!OK<}C?$uYfckmaFqeD+Qpi7mL7dkhl$#h5P8cqn4HjqlM8boNVJq{kZ->Kfd+&xhl_e_~6yHR*p6PVedh~YLy2Y90`~2}8WAWHh zhZ<-LK#_5oC!v@srgm_oDl$Z+QB;XKLC8K+-vtRIgQDd_FwFVTb!w^i{p7`6ZPu^5VhV$c5ib=tcluAILWN# z8}`* zWP&y?wpxV)eGma&ax%yk+hHH;uDYuT%{+6pU-9)p$g-ewckO_U_Ng(cDh0}uCb82Z zW*k&k>rwko6`wUNWJkFo=8H!OhjXD-Zn$}y48}n#Z(-RLtzF%LgIxG;;-4RQW>V)xfwgi7=pbtPR58hNrh2Z?8E3W-hL z5K_khZUuA*K_M#|E5%w2@(G&$&8e9>Hr1OR>e@xkvQPm9Yy-Bd`-8^y!l+4sq~S0M z_cW6bYGxx5G?edcu5!Y{%j|#EDzhOw07s0Ngib#c7ax&4oWS2Z;c1rsVTh-F^TIov zk)J~lWK~cs>Ej({fJi8Nc+)hB(JjQe7x9|FcIb;=lCr$U%HTiW!Z_Yk)D-hZCGk!M z4*pmLppy6?%?;M=LFsdgm3dz6nU!%imKnAnALI6!=Qsf@^vr5UpR?i^4(m;3Bz#ke z9mWOWB7ox>nMG9eabkjz6EsyJNqql1IFopf7@lwC+UFiYAuIaiWjQC7d#s_Bn zR_M9Y0Um6Q`=fE$wtHMoMKhnaz9Pa1keCW)c4$l2DPj0P9 zg9QO1Sg4HNL_(x!rupuKv2ASBTq3BcVvQlF7u)%9{9>99cLpPqPLtCDLCR zQCFZtQ;SmF%F45KI5MwfIWK4IopghaIS5O%RbI!MtCC zReidTIay~k$-2`#T)PLz;+Wuadt2mzQB;TvrwjXsxAHc2OIJ$~4q^_CG$Yrl}%C8Jj{7GR2C4t;XrVMWgPT$$JW$*6LbT zDeZhiSUpMau@!&q#};kD%QLaZ`28 z>l=#q5e6$)SWxsSD$HD?Raw*sP>razQn|MTizT*4aTzIN;Xx7z12bODIsEgn@%O@l zkvP4=s}F8*KxyrOXLz_c`|X{@JD9Wg{kM-!rsrDMcnF_*w)ZB+a^#{b@XQSG`-O>sL1zI`V zglc#k{RnV#>I8=&oi0axn;g4Q=fp#{htb4+F3II7$HIYo1LM>bX-mf~cd%J!p~k+0 z(JsRE8S-f3_*=r#J;)TVemAa&qviMfyez0z661%zGBlcx`3P* zRJr(F4hdZ#VJG9IX^R&!OeSHiY&e!+^w~GQlCSgX$K^SQY~1NvrWNj4+#Fo483%WS zQ+6b-8k1Q_!9zlZ{d#3G0KPqCQNYz5V_tw)|D30kl5SW-)L2%{6k`|l?+Rpi`?+W- z?H<_K?LSoMK!0-sLUQQ%pAavx5Q?CVZCVGJuyOgo8ZH8l5PBvp-2&6Pvt#&mU~|fb3@V5F- zifoNymm>v<+!7BStshei)9KvXMb`RPYzueqa^R}0gL(Jy;~)!o#GOcrIDKesyHsvP z9PQD-jMPD!^=-ZyES`Uva_QFv1NI0B<4Lns!Aq1_C37~5lv_ozVjWyEJLyoan^s=& z*!kYvTS8GC`~`PI$%nKr=_%GMSDqa3c=tj$nFsQjC-x4de_Gy9pK_vFkmS~DgNx<7 z3g)Ga@jiak5cdSe#H?;R{hFUCRTaa@jqW)p}Sl^ICL^X{(z83H=C--$fd%*Jws@rRK_N*h+p zEm1Axn71;e{S!G`NUzy$#G^E9dvr0Y+EQKw|t%lUAqHfp*;9myNKqkpogeSkWADuCErHPAoz`gGRjNWs}+ zxO%RoUp|`$R>%T*gmwnS)u5XOmm8(^bcU9YB!aR2>*u5#Ebe#_$=Mr%cMbbCD0g1tl_ur`~^n_RsdZ3ltH*9Ppe`$EHXRsG~A3Ps1 zz=X&izk#&L$Pjw|kBy?V13+fB2YWxaP82H&S~kOMYocD_mC$EjK&x5DTf7c5v2Jx= ztNs?}mR9eUTJo`zIiA!CH}jzJFx%_0^(edZit}jn@8{Rn7lb|gXDLybf4B!65$Y-f zh|~Zl98kgT77R@U&cUv3IvV-hHSwn9nS+XoN*gYV!u*)O5%0*L+&vEuqXu&*fy%eU zVJ(Km`kxY2_F}9cQM6hAvo-Ei*o}w5jZy6#`NcD(HRDH`{k0QXcj#UU_Um=IYsVjq zpb04|UDaue6rC|Tl}CEn_GlFWHHln)N6PCa$Z@ z5RgNOB?L;ADipWot_V8Wyr}G_ne9Uv%~9m8L|<+Hx>}P5>K(Mfv}2Kf>_Tx) zI}w*hsuj2P+*mws3?W2XW1SRkQ@7dL=x>2VB)82n@a3lG;!4Ym~}252iiV?=Gvmp-ND@|ow2LSaUlOR>2Zvj%@Qv4LCkV8A=mS5fa@Stm)Qae zmH%6rR#h9UA}!9NTBYG8s#>{bl2EczZjz(qx;0|QKLDOay^Vf{&7EjrvK$U`B|2E7 zXz_%)MXhP_D2wsYk9KvIBa+tMhI(@hvhh39THXE;eItmKe?cnpm$1Cdm`9~(6~OtN zicak|*di}`5fq7e#lqUCb903%Q2ng9DXAFpD8mKqAge>u`;O*K(Kqa!7|7*KuBC2B zZJX69E~i5xsYH{y0o!H{ogw@VLDS@!Xq+SuS06l$b88KKx5ZkV09ZB}`bf^`S_<*m z7yixsT@}3UK`ScVr5i}!{JrMqETI2=Wo+C-BhlQAm$vc^8Nr{V_tNKpXue%J*rwMH zD~is}cq#U7T*0M=m*q4kX&yW=d*z|OH|SeIDw63@JSmGeY?}rmPiL4z26%7Rzwxei ziZZv?Fs3 zC5E;fB!>7H=0**E3c&Va#o2}}ZuWedzsSKS(^YW?E z?81LCN#C+tVZtbk%E#`}1nOkg`=8ylbC|6x9zEqn?csX|+_L{DOu<*S%%0Q@i0|~9 zK4%TACKP0pWORj~8sHdcqMmlu6>Ik&S$8>xrQ4+la)IR)9h!%8o&}nI$Qv)IIO~WB z_QnPi?)I5UJ6I&^efkp;&l;Gu zmyM7$_2}llUUVKmU7oN?cUX(_!Uql{_4g4Uf?&ixmRH6|q)$@<^2;|yNxip{l@KU* zQTVO6aw0!0qh$o~6Cemai5L{+8Sd|iTrR7vqJGnV^qfY&!Xz@B4UO*k#3zI6`wMjY zicVHS5m|{oe`b6~ybx4VU2>vlFLG~YA;Ma>m!Nv~kq$h*V3hGI%ve|t&;;SoZ6rxv zf%{5W8@FT)+Ph8VEk0x^%fund4F>4=I&Ci!msFlxT0M5bU%-93 z;RGhLX(pCa;JS<=H8H-4GM058IBni|vB8MGu&}1tHAf{xfWu{GFC+2H@exrSo~^!$ zI$m`>mrNMkg4qKD>#k~B9e1_iS!7P)@f?wM)m({0z#GXGpp&_On~U7(tM3r! zI77xdW>|ZTlIaZF)jLs3wmMLlqYv3V^_C`phc-ak3kZ(`di#8$&luJVt>6nD^C}G| zdHAP9d?%^(OK38Q2s zsb|&={y$1({^uL(AO1}LfcdY-2q-O=Pnh)A0M>h3M?{FM?wX-dYj&L{pGNp0CiMp< zmcYntL18XhByX6yS-ew!Lg{*!h9L}^0izW5@^2>VdK&Z@7z10~E;GF@*PYC|-(R0$ z_~Bxdrf&#?&>Bf<@{{wT<6%==lwEk{YsX@(#l;|Hg4JuC`w{J0!Fp;IcEu`U~0=vSlZarGc?m7T%lxKzjRvg~7^s)qC%@Wk9{ zGv2y#0*~;P>ZQl_z_e0*w9*6d9L#dcx(>7HJ9?2vxdJv3+?=Db43AR(I?f$No5;pJ zyoi^|a9&Cf>J%5frh$z|Kb6+|;lg971K&X3$~S9|p&njB4VN}Av&f?-=Otu9M*hUw zTHH0ZrHkz7{Q;EgSa9-SQj=l>+fO-Sn_k4a2ECtT@+4h0YL_tR3$Sf%tJbR|nPPiM z!MsYamZ2!qTE$KD@o>2C8F@ao82=jmW$Ul0E%_CEabU?6#kP z_g(X7y!&i9_#1>T8dCN8HEmEbvbD>%a0|3}&I$|#v!kfDh!#j8;fcq-XM7+DoTYTCtRu{Sm`v= z1)5?JH1oBZysBgo1)>R>cxNLi1k(kw2?JbgRN-u;O9cW$t(Fd9ERZIYzX`|YM{fxY zB@>ucoRaCN3nZc@7mZD@=#o_gEEbI$v70DntbqP?swXXU6lV)=y%^2{HnlVk`Dl(YBm&QEX5L-lK)Z2cSAl;_YO3pwN*MH!Vrl5ot{}EB+c< zP>M6c9n49QgKw+IA7G}l(xma})@b8)%r;g|>rK0^ENB~MXvi?58yl@}bg1K-=vz2) z=fIfu2#j=zHA&Z^@x;Orkwo_%iA6ee>8I37ROKcyn~c1Tg+=4P^}<@3UP`L7S&gi% z!VGh1oaE^ZN?8FL^W|HSV#E3Y3mM{?5O?X;V_Z2d-DwTC`?{n5#xs^>{aQDXrKMW( z*a0SHP3MeHX;A^C<`uOYxazdfE~0O{ra(&u2_N=@>ZX>zjLe%$$4u&-q;cm~=Thl$ zsSC>}5o|@Q8^}>wRmTffBTj8t5u}gJVhHMS4L6`GM5#ODM;?Ev(U7bNEgn*%%_xK5 z^Tf1QW`y&Z8-HURuxLz`sQSe@H`zw6n!1hWeSy->xU)2>%4;e~bw95nBS~RNH3GRH z-`XqpNyB6Fnn*mymkUjq%DUzfy5=t_7C82Vterk8`j?C!B+Co-lQ3jq-@z;+w$IbE z26QWEQ4g+T_Mu5a>auU-U?27Sgx8#NhDHOIO#G8rIh8X?!|Lq+OT_Nnd%#SlPvb0z zwDQyapvE_NziVM2(Dq)x7qqGV*--B-MqU}FypCofsJlO#!|(IymS&09V-7*VpvZi zm!Mo9W?%n-FLP3a1bl=O(_BGXx*0X?^-u}S+pO>1AANzY338Y}r=aC>f=#Pq0Q#KA zbsy7Qss#T#G!(s`*|XkW$Uuc6Cn=f9&n96S(+7@Q%f z^o#Q!=%d>|R#lsDp`8&PAIlOlbx6zu>ht;^%A`_jFGp$$WwDwj*N7WAB=X7JXx#@I zr=-bXBD+%2sweZv@p3YL|4z11t5$DQuBkEIU9UO_(stB8d7ezNJOZXAtY|0;8>?3+ z(u~d$jNz)N%48eNm=IVKb$ZpFWrg(^BT$nBmDh)>( z|1fUQ1QvvcwfoZ~N`NCt!P=xnig{YBzx|6~>+%N_&iV;DH`i7iA;1pur>pWt&0!N1wM zI`svUu|%z+NZ#wQRtH@8RrLj`*A_0a*xfjEdWu8m<2}35wZLY|G-(pJR$WR@Nrzie zBc!AyK`q-@9Jp2qUV~xsq#O3JA40ZqT5vCk+GfKxr|xF%+bvu#dmcwl60>#gy#6K$ zG(m%ZvAzodlpp4hxnVjMdvoQxd{YFxD#h9AVEc+FF2XPplyk3PjEBugwze0IXK@uV zBFWrvZZ!_3;(@PJU|W59Sht)9O;ckSfL5`~+5K2Qwf%@-@6tbFkGqWfHQ}^df?ftZ z^!?g@*!DyF0j+9X(nLSA>J5_3T~(4+^@r6K490JR4P`Es?w{`3rYIw!8+}Y8(GKFYRCoeXfoYAFsdGh2OZ+n;bvjZ z?K16sT(OS6Df+Dk5!?jKR+gQXE4RK*C0f4)6M&_FbG1cxXAAvXH8uOGdgAF@Ms+C6l;3U%z4Ys>#~J;Ohoq{Ax?d87_w5x z4S@X#PZDMLD&)n;D{gL4#ivSd6{u%X4i7HP+F=^7O`zV=_l2(929i*Tq`P=w?uQ^_ zc%zY68Hv6TWRoM#=cxPMhi#6Fjtu>j^`IT$T1K@9KVTuh!4!2@=O-R3$Di2@tOVur z)e>=`XMduSHxeREeT}i@uRfFTUz`*{Xn^jXmyN;S(&5qcO&OyRzG(dSM$<1ay}2-o z?}eAu7(d`Q_FRFQm_WiK{1tG9%(uHf3*9sheB95?bWf zQNNyVod%(@x&(g%{BBoH3wy3g3l6wrE;imZ!3f0uHMB84w`cR~gResS$8ZC9$5hP_ z8tm7wy`z8ikq#Z*K>RaskxO5ot5a+!UWg(TcJ*9>9*xdV6w0@{fiA)giM*>MWUYy| zL20cVDm8a`o^tdkcqyO3;}C{nzX2w_kXE%yrf?|dwhhP`oH7*oFpsfqmo$@xs8Ebi z1@>|DK6r7esshu1-felxjI_Un^^)VF>;MNkMyZ=BPb89J3PhMakx1j#1VSciI-QaF zszbX9nY~o27>`kgP1`jcMWO~hKU3$dY$a!IKcikVFH%uqn(z|3iKbhDb|fZ7KgC_@4NhZtn>O-bGYfV=h2lqWrn=TywtSsD6Hq=7DAm4Uau!e>p5A+5pdmazur_ z(--iQd{zqSWHK&m!{I!j5Ux*Mqk6Y=KCGBre(I5FToeOcA1c6^95`uKqUS$munT)W zkc8Q*(&DH0t&lx39Uz9-lTdR_?0K_v+{b6g?mO=Nf%mu#{y2)-zmIx9rrlo%3sX9< z;K&v~3!~*qOzKr52U(+6)%Sxll!!CjYG1*ZVCdl%V4IlYkkApWwU4(>b+woCj_x)} z@Y41M;+9fEBVWrm4g|+BWb;OQs_lfjbKt|@K1L$TNS`Z7+o@8-NRiENV7?~A_ori1 zx-+6Di4E~TVFLP|;$f&XQF-}n2+-@tPB zvd=toX6Ah6?!-A~kETtD)WsFFVsbc|=<%J-GhVM*%rCtExajO>mIZlV&M!-&`ex)G z{QVG>RehTYy-tFJ?=Lcr_ zDU^nhoP6XcQ&*i;s?t@)wAnQst0d@Lhz?;+jWjiUTpt4#39R`rKg%AD9)xXKhDzh9 z97RlTWA~)do|lm~ZJ=+^yYGhp0qu6&pjRZsY;+47?p;`}51LoT_Kx(dyFeb#;T4 zCeEcbdN%l~RqNJ#Dp6AlHVJ+SQOYW7U9`8=Pes-BSs9l(-z~XRpLEd+W*kHxYkXsZ zuznYba4>vA{9XNkxX+!iuH>27xCM7X9K*6!bvMsYmhAGD;wx9a%*I+jkPz8;KnG89 z{{=YYmI|3`z>Z*fm|zZ{vrak0B^}(Fyg4W`hoYZjj znbi-$Km3BlEw2$UDD3MxTU|@;jKbI(e6#)>=|zk9`r zEq#*cj78D-(B~;c7Ig-q`12WR6>;dRofXrghl?1x8^i@@Nb32)nQGc(dS8rgzbs7A z8W+4g4BRQJJY#uzA;ecH>U$c}Ke41W34IO++13b+>{$1$h->AmUDj#ztA)hqY@3-` ze^Zy9L3VGL73%1!`({0`X(P$2>>I6Q>`U7MF%W$cUxB~Ur}P_@?Ohg_#!tKjGH7?| zka;51da-Pw_KS4#Qm1Uq@4(X;pURci?AQBOYVpTcZ-W-Fqg#$KA~V&09jd>+c<}my zSIOD*(_5T8eXo|82Va%I9zi;gGRFOj;}OmDq{={Qg^h(U@zVebKR-7Zo}+?a|lg`!A0ZFgtS_!_KlyZsn&8f%vLF#ddDt?(g`$ zGx^b502qld0EgGPV8%{j$v6;*%Cs1$zkN2!%{34mr)LE{Peo~7O>6*%^6T-_FR!w;y$&<&AUD4*;Ki^M z2{n~$<;+Tws11ATLZ~fyl9M&mG&#oK6*osA;(v%^VGfy$`}S$Hx!(>FZJ|xF=ri!K zP)mRLRL@jysVewj#T=9s>5)~!PMx$w*!gOzs{YelU8}6mG{vyVV7W_Cmx%se4##eL zT^icFKAX?5K4fFD;@w_2W@5wsvE?-7r!Qmz*j#d5L=hE5WvA#je$$LsUrzO9LUIPD zm~Il&p5VLH?t9n2XH#~7uv0hcBn`SLah^3%2B5|rI7Rw z+ljtrIrpYA-DpC8G6gO!*+#_RN9C_`!h^J*IFEeaG@U>hx=8oICFpxn8zj!uqHpx6 z*=qPR=QCAt3xREu>h4s9xa6i>R7s#TLZWUF9=yTe{YxO7pdmS4)-z_`Uu?CC;QX+5I`Y z+HG@y_lt`xY5yCsIEb#yxHDSt1Oj0}QrzX7WdS)GSfERF0WFxB?qMJD^S6;&FT^Kf zuc9|KByS^wMyj@_pRGdSbgi4}{hKi93%IBmsIz*1V)_b|=|MF&nACrfYC*a738wbF zsI|(KKfl;)W%l9TT%sT&5G^2TF%i1r^uZit|MWHAiDHhIAxaV~Jg{S0N`Nyhyb7NL%XN$-8`XlORPt z*@JZKe7PoT8tGBUB(eDD^40dT(5pj{?Xi&t(X94|^c!Hdv7GEU4$ZSX`Ys^qG-Dx{ zFBSYgT<~GDHgQB*scg1OX&2h;9-FLBkoa@YUbmGFm1;3Q=tsW^cOT(?4Ty<2g^BlYGIC&-eojSYSF*pWtkiC2C zG2ANdi+CFBD4~%vdxHA~4A)(&u`8USqBll);VzS0!(NNlG`$M?<@v!DLA*kkPx4sd zCUUO@MN8+SL%Xd=b??MmAx0(C(2&y!9&>i`5kqvFot{=rbtjc>VLXt`f}-dGaQ1Y| zt}#X$J&B{QLxGSLtkek~GL-^(&$V;=t7MKw}1Fy&ItZF>y+2W}IDiL`BnOG{p7dBHOQHr#-68jED?F;C$dkI}Jv;7TkpByzj#i68-*8jdW4zzTrZ15(WsZK)W8sYv}eBC5X zRSld|qd-VV8j z@4)q8T0_I8TuzvXSZtM7(CrHk(klDD=H2?EEi^tzo0=N(r4tGJbM<5hM)iD1OJjf9 zmADUe=i74fXO5B>q{seUX%2_fER@{^jHa?ui8&V9(>&hMon!=tR+wJ_hi((GGp=q0 zRqf@e9`OgnHWKnw<(r>zH8#w280K11d)YI%N9>vH7*o;-6}RhjeHLq!X&fq3WQ=$m zls|lpy7rZZj5{@)hWLpod`buJ43+fDye;+8=W-31j-A}hbjj{<3D1lwu-BNbHDCC~ zVW{dLJzPu>(uVj-dtt^PrrbtyWwHhVN{%7B*QqV8ub~v-`uw!k=yhwVzzJpHuqM}+ zGpPfyam}2xV)!+Lt#P7w`^4<-V^t9Y9Tx<*>SR8xVHXJuo}p9~qTERBCI!`X+{M-W zFpH@@i*4!NI0HtR@rq=p^`4@5@D$fgFL*3hxwP1_RWgc^n%8Krj9HVE3hwqqOG zyU0wfhsb}N;U3#_LpS^xp5*nafVU}OcLcI@CDY)0A*Ci5CP#q4RlYps$=jb+%JTF} zQe^Fp*-qIHCNoTY3$(RdWaU24g_Ll7YsvuQu3MtAVK~6TB*&5QAUjy<8W?tMMpn?{u9OC@mkc;QA4P9jwIces?;;8)F;3y&ttqSpg0_B2H$hNlmxm!_ z2UjA4##I@i%b#olw*b6@TzOgjooZ}qfb2=UQ?m{-QVpr*^D4+Xz*WbFS1j@61SBr^sLMeKys&0d+BJ<=v8GHn#$wQq zCe5!OmTu?x&NBMfLz`t60ne6K{*93`-qEs;m(Hbl`AIXJDUS=<LiJAIO_+Ocw1h ztbOVu9okJ`tfCodsiI*vsYc8n5B5Sm;&<6KzCWndX5~JFhi%dF4Te{yKuZb~AcnvJ zbWD*3!{!6`YTTSRp{5KH!?HPuu^712rgDVb#Wn#i8xCIJH?({`{2Y_D&UW03q%0Lh zeFF!HcXrgXmAAtSW6Q`$;o4R@8d4-}sF%NFRNlb2=^T#;&GwMH_;4666sH z2Bk8P9_t9dbZ*IH^8zL>-{yUYZcT{J=H&muY$<^O6IRV?gR)uTPoCcNzJw0 zPd@QfH{3xe&ZhFukB-p&JQ;&3=m=Ko4Ub;b)v7KuBOyW?_8-z87na;l?mAZ(4bQR! z?c*ledVs!;_j&m|<7WRjod6-Ovt!y$r|ACDo?Z~{jb@Vk#OPo|-Oeqq(3^8EEZfj( zXZyoKAYbOHg(FW?JF`YpT8UzYjI%-LdfsHKwaIr+Si6U~gVhDISIl)ncn5k9GB zqDz`l$yKSLsWD3nlu!tv)Me(&gz~eQjaV5`8Rt?q&9kXKtNTg$&W>lf^Qq0LloP^6 zyj5fP}JOaQL$`j{>jt+{Y8)VSx~TCSxALWZrkd{`egS1{eB+7a>T+mx(HAGhb1m_R4k(!~oPhct5bqS<{=Md>RR zJz1hx*Mp~WPqA$sP&P|8O=uOO8$dXJ-yyHi%5SmEgvS*ZdCJ@^{1{|BdFJ&rPez!B zf4-_S0#QxN&X8x=L*^VbmKezsF^1W8Rap2e5^;Dn7FcRD3QZQS(cH?KS?}9=O1b`e zO~R=;)qq5L;xmvBs<^S^yY&_u2*jV`qC~on$NKdy)KrAd~=zW!MXXYT_djD;q0ui z5R$t3?h>z;f(Bn>fqIMZ=``V#;2Et@7w#N3)o`%R4%HdUj*?K$D9*1+uIuR^JCf|_ z36UEnbS@yvUqBFtc9HEx6|0zwEPRMC{JUNe0=jC?$W$5vHD;GhbGb%(GXk==PLH#k zd>Cn&h)W+{KvK{Mk_0Ny))@PF2xgrn``AAVoN!XktXM%(*Ifak94ATX=EOEPemlyI7 z3U=c8%J+E-=0Zaly(dW5U9*D0bDR;;mO{aAqroY}#_W-vW^8yYf}3`Bf)L{?H4VE| zJ*|jnhvZKN4K!;iABXvzk!{_zG+A_A*<*8q;vP{%fy)3yPYCQ1Fmn@NB>I2Vj zf?bHE5h3ito=nA?$ z_bxUsSDz$`x?E>AtT^@kvK@q9x(nhtFT7C28TUnGG}l@J^Uump56A0x3n_b}B|Zuq z^aDZco!@2V`0m}zC?+(hz817|ZBGp`wvxb2SFf#zWmXjNToy4XcK;B4F~_ui{R7cr zZhLn17UQzLN13db$@DBcd`c)XxI4FXnm+W+`-?NcmfLdHnl#@?zR*vSJVoJ%{1SB( z-ZN`q!!%0S>FIZX-{Thz+-loJ^2uIL^y>mmk0?pI|6BDYR zOb=B!l5a?a-?X#WF-OTmsJ^AdTt^PDZmNeD=1{LC(6xYArK1%g>rxEK49}!Jk*LN! zuo%KfQaE;TPcrBnnu|Ka4$;)KIL0hFI&0)=1vck^ILr73zfMW3d3{MVl4%!A6?Qli0U9}y?VFChvjlJuXI@Ii#}&Nbr*hNm&cfT(#;{e$K~O={7EtL z>-X3z5!S|Mc*hjyrTI(pub*VmDyILEmlsY-ip|xjRuJiu#2nw^3GF}Tbi`&=xZYNW zTT#6&r*40Af1HKF3Dt^Lj68rj;7PBQhrk2#_y%Au#QA4Us_H4E({Wl1(|2Rec`u*) zah;er=E4NEGLeQOeJDJZoSa%je97Bo-mooTOY2jg^s$OTv?i4U|rF~@ADFyD)M8ZaLgLJBS? zk~=zfnfWnOz4NveqJAg?kClmxyW?47-DlOA{O*>1W=`XyZcnT3C3!)SU=AooWZB-0 zYOAFUyL;jl5_r`;cT#R3=kVQUt)ijDpmjN)U2o#z%N@!ml=8Zj9^<`qYatT99NDz} z&laOJBv?Vl1?E8~Ji{r%UAELk6ZZSq3dNAXAG_(auNGJnAzXo@#3!-Bk(;>l4X0R%IEU0ce$Ljc34i39jnvKoot_Fnsfyju+iyta zy+E0h9h8ZvYKY|4zBqD<{M>s_{-8wWwM1%%J34#HV}-}6DGn%8Ul}DO`G!-D z*xQ!_^yE!=r{ZyFzaOrXhCZ16PX53zRCy=j`%bDMSJ=;wqh;K^k?I4vAwHZ8KE0ol z2T~d83omtNeVB!%X&Af{H(#zjSyVRnEDF(@UJEBYtn~$r;Prm_>$_zPZ#tha)UHMj zas({#VBR|D^EZ;JGLqlOmOWbN`au2=Z$h1!JlIqf`?tHp_-oa}fpbjbPNZWI_Q~ux zPTBL`IWJe(UF5n(Ums%(DkNJ?SaucDXBVfJbB*zUSv{a z5bp6iyFO{;WXxl^j(lXcLg`RX=GCw_5Pr$La~UQ?O0+)4D9d!Ni_y2rDe-Yc*eYKX7H*p*#Q85| z>X<+=9D7Gjd$d+3lep}?T}qhiT;<(AH+a~aKC~q0sPbYUYjgKhYvDIIqVx$nu}G#+ z9Tz}!{xru?YpcG2JFNxg>Y1kMgv2mm`&i>2Yf50hdia%wd6hs?rQDufX~uQ4pskpw zstM6ElOXu#R)@MT#EsPJe?n&XDYBo8 zmEuVn|9mHB#XD2x=2>q*Z4xZ=DQsdni`R%Xhh%tL>EmdN?(F+g{DU6|h~x9Peu-W- zd)JjS!Q+A9q`!t6CcWSM^zq5Y+75{r=S!U>sY@&RnD!nA%{PPzgmTn<9ecs*I<+w5 zW{-_-k7C3d#Tw@p8@=z0JNp2VmNzKH8KUo+7WnId@-b!|FoBFL_cGO^LSvhRU}6AX=gYnV z;t~Mu$9@k4egb2@#w3A~HvbWW>AAyWLSVDU`3cMz_CI*Of5KC!e*+%Wq?r`Py4pa` zb^?Gh$l>6C-~Up+|0yAiHH-uu+VKx7EHF7BLc-&}<)mVSfg6|!p+qfzM7S61_di}Z zxO{(hHUP{B)oc6%{5v??zu0{L1_2nWiZDKO@E=xKcnzSLCei-x3~DJD7^Rg6R)m>Ajz#)lGj z{^9!Dan}F1w85}~7uxAzg~Hr)08Ow6bTQw*DBzm!&tn4s8K8{utaUOdgpr=uRMFhcnAXGmnyuUE?S=XP&fEh6WqCWyQuw?E5S0FIa zqq;a?jQ=}{{&%s^002lC*NqLs-P_&0N9YKJ5sD>a{~ukuw~cl$lX)VLi3d8+{YTvY z?N`9T3EcxKr2zUy07dU%|D$u_J$E{%oe4AvB^1&Ff(Zv(4+Lsk0`%zJ^Wxu>?{AO; zl*0gB?V*GL?=8aoivq6s{;K%^kQi8#A%X#6YY9Lj`v60%@IByk0j#>|y>u`jFx^PG z>yh6;@dEdNcqLGU3ZegcW1@-Iu(;l#|qd@UPz#w?PJ%lU%fRhoR r={30jr^Ww!-U20%-796X8j4a+fv2i?_vC Date: Sat, 13 Dec 2014 00:50:40 -0800 Subject: [PATCH 027/857] Apply java plugin --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 4e3b950704..4d55d340de 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { description = 'RxJava: Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.' apply plugin: 'rxjava-project' +apply plugin: 'java' dependencies { testCompile 'junit:junit-dep:4.10' From 8026b41bb21e7e7c1103d0492e890ff0093fd6c8 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Fri, 12 Dec 2014 14:23:48 +0800 Subject: [PATCH 028/857] Fix the issue that GroupBy may not call 'unsubscribe' --- .../internal/operators/OperatorGroupBy.java | 53 ++++++++++++++++--- .../operators/OperatorGroupByTest.java | 22 ++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 39cfae4457..7be49e450d 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -19,6 +19,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -34,6 +35,7 @@ import rx.functions.Func1; import rx.observables.GroupedObservable; import rx.subjects.Subject; +import rx.subscriptions.Subscriptions; /** * Groups the items emitted by an Observable according to a specified criterion, and emits these @@ -76,6 +78,10 @@ static final class GroupBySubscriber extends Subscriber { final Func1 elementSelector; final Subscriber> child; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP_FOR_UNSUBSCRIBE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "wipForUnsubscribe"); + volatile int wipForUnsubscribe = 1; + public GroupBySubscriber( Func1 keySelector, Func1 elementSelector, @@ -84,6 +90,16 @@ public GroupBySubscriber( this.keySelector = keySelector; this.elementSelector = elementSelector; this.child = child; + child.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(self) == 0) { + self.unsubscribe(); + } + } + + })); } private static class GroupState { @@ -138,7 +154,7 @@ public void onCompleted() { } // special case (no groups emitted ... or all unsubscribed) - if (groups.size() == 0) { + if (groups.isEmpty()) { // we must track 'completionEmitted' seperately from 'completed' since `completeInner` can result in childObserver.onCompleted() being emitted if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { child.onCompleted(); @@ -150,8 +166,13 @@ public void onCompleted() { @Override public void onError(Throwable e) { if (TERMINATED_UPDATER.compareAndSet(this, 0, 1)) { - // we immediately tear everything down if we receive an error - child.onError(e); + try { + // we immediately tear everything down if we receive an error + child.onError(e); + } finally { + // We have not chained the subscribers, so need to call it explicitly. + unsubscribe(); + } } } @@ -187,7 +208,9 @@ public void onNext(T t) { } group = createNewGroup(key); } - emitItem(group, nl.next(t)); + if (group != null) { + emitItem(group, nl.next(t)); + } } catch (Throwable e) { onError(OnErrorThrowable.addValueAsLastCause(e, t)); } @@ -250,7 +273,17 @@ public void onNext(T t) { } }); - GroupState putIfAbsent = groups.putIfAbsent(key, groupState); + GroupState putIfAbsent; + for (;;) { + int wip = wipForUnsubscribe; + if (wip <= 0) { + return null; + } + if (WIP_FOR_UNSUBSCRIBE_UPDATER.compareAndSet(this, wip, wip + 1)) { + putIfAbsent = groups.putIfAbsent(key, groupState); + break; + } + } if (putIfAbsent != null) { // this shouldn't happen (because we receive onNext sequentially) and would mean we have a bug throw new IllegalStateException("Group already existed while creating a new one"); @@ -264,7 +297,7 @@ private void cleanupGroup(Object key) { GroupState removed; removed = groups.remove(key); if (removed != null) { - if (removed.buffer.size() > 0) { + if (!removed.buffer.isEmpty()) { BUFFERED_COUNT.addAndGet(self, -removed.buffer.size()); } completeInner(); @@ -342,16 +375,20 @@ private void drainIfPossible(GroupState groupState) { } private void completeInner() { + if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this) == 0) { + unsubscribe(); + } // if we have no outstanding groups (all completed or unsubscribe) and terminated/unsubscribed on outer - if (groups.size() == 0 && (terminated == 1 || child.isUnsubscribed())) { + if (groups.isEmpty() && (terminated == 1 || child.isUnsubscribed())) { // completionEmitted ensures we only emit onCompleted once if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { if (child.isUnsubscribed()) { // if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up. unsubscribe(); + } else { + child.onCompleted(); } - child.onCompleted(); } } } diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index 9bbed5d04d..5203a77af1 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -47,6 +47,7 @@ import rx.Observable.OnSubscribe; import rx.Observer; import rx.Subscriber; +import rx.Subscription; import rx.exceptions.TestException; import rx.functions.Action0; import rx.functions.Action1; @@ -1385,4 +1386,25 @@ public void call(String s) { assertEquals(null, key[0]); assertEquals(Arrays.asList("a", "b", "c"), values); } + + @Test + public void testGroupByUnsubscribe() { + final Subscription s = mock(Subscription.class); + Observable o = Observable.create( + new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.add(s); + } + } + ); + o.groupBy(new Func1() { + + @Override + public Integer call(Integer integer) { + return null; + } + }).subscribe().unsubscribe(); + verify(s).unsubscribe(); + } } From 272a7521188e4e174e5903b688be390315d470d5 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Sun, 14 Dec 2014 17:24:18 +0800 Subject: [PATCH 029/857] Propagate onError to all groups --- .../internal/operators/OperatorGroupBy.java | 17 ++++++- .../operators/OperatorGroupByTest.java | 47 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 7be49e450d..16de384888 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -15,6 +15,7 @@ */ package rx.internal.operators; +import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -138,6 +139,8 @@ public Observer getObserver() { @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater BUFFERED_COUNT = AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "bufferedCount"); + volatile boolean errorEmitted = false; + @Override public void onStart() { REQUESTED.set(this, MAX_QUEUE_SIZE); @@ -166,6 +169,13 @@ public void onCompleted() { @Override public void onError(Throwable e) { if (TERMINATED_UPDATER.compareAndSet(this, 0, 1)) { + errorEmitted = true; + + // It's safe to access all groups and emit the error. + // onNext and onError are in sequence so no group will be created in the loop. + for (GroupState group : groups.values()) { + emitItem(group, nl.error(e)); + } try { // we immediately tear everything down if we receive an error child.onError(e); @@ -259,6 +269,11 @@ public void onCompleted() { @Override public void onError(Throwable e) { o.onError(e); + // eagerly cleanup instead of waiting for unsubscribe + if (once.compareAndSet(false, true)) { + // done once per instance, either onComplete or onUnSubscribe + cleanupGroup(key); + } } @Override @@ -386,7 +401,7 @@ private void completeInner() { if (child.isUnsubscribed()) { // if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up. unsubscribe(); - } else { + } else if (!errorEmitted) { child.onCompleted(); } } diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index 5203a77af1..42023508d3 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -1407,4 +1407,51 @@ public Integer call(Integer integer) { }).subscribe().unsubscribe(); verify(s).unsubscribe(); } + + @Test + public void testGroupByShouldPropagateError() { + final Throwable e = new RuntimeException("Oops"); + final TestSubscriber inner1 = new TestSubscriber(); + final TestSubscriber inner2 = new TestSubscriber(); + + final TestSubscriber> outer + = new TestSubscriber>(new Subscriber>() { + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(GroupedObservable o) { + if (o.getKey() == 0) { + o.subscribe(inner1); + } else { + o.subscribe(inner2); + } + } + }); + Observable.create( + new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.onNext(0); + subscriber.onNext(1); + subscriber.onError(e); + } + } + ).groupBy(new Func1() { + + @Override + public Integer call(Integer i) { + return i % 2; + } + }).subscribe(outer); + assertEquals(Arrays.asList(e), outer.getOnErrorEvents()); + assertEquals(Arrays.asList(e), inner1.getOnErrorEvents()); + assertEquals(Arrays.asList(e), inner2.getOnErrorEvents()); + } } From 83fc06f331c448a6aeb808592245e020384ce5e5 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 15 Dec 2014 14:28:00 +0800 Subject: [PATCH 030/857] Add a new state of `terminated` to fix the race condition of `errorEmitted`; Add more comments. --- .../internal/operators/OperatorGroupBy.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 16de384888..b44e46bb7c 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -79,6 +79,9 @@ static final class GroupBySubscriber extends Subscriber { final Func1 elementSelector; final Subscriber> child; + // We should not call `unsubscribe()` until `groups.isEmpty() && child.isUnsubscribed()` is true. + // Use `WIP_FOR_UNSUBSCRIBE_UPDATER` to monitor these statuses and call `unsubscribe()` properly. + // Should check both when `child.unsubscribe` is called and any group is removed. @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater WIP_FOR_UNSUBSCRIBE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "wipForUnsubscribe"); volatile int wipForUnsubscribe = 1; @@ -124,7 +127,13 @@ public Observer getObserver() { private static final NotificationLite nl = NotificationLite.instance(); volatile int completionEmitted; - volatile int terminated; + + private static final int UNTERMINATED = 0; + private static final int TERMINATED_WITH_COMPLETED = 1; + private static final int TERMINATED_WITH_ERROR = 2; + + // Must be one of `UNTERMINATED`, `TERMINATED_WITH_COMPLETED`, `TERMINATED_WITH_ERROR` + volatile int terminated = UNTERMINATED; @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater COMPLETION_EMITTED_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "completionEmitted"); @@ -139,8 +148,6 @@ public Observer getObserver() { @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater BUFFERED_COUNT = AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "bufferedCount"); - volatile boolean errorEmitted = false; - @Override public void onStart() { REQUESTED.set(this, MAX_QUEUE_SIZE); @@ -149,7 +156,7 @@ public void onStart() { @Override public void onCompleted() { - if (TERMINATED_UPDATER.compareAndSet(this, 0, 1)) { + if (TERMINATED_UPDATER.compareAndSet(this, UNTERMINATED, TERMINATED_WITH_COMPLETED)) { // if we receive onCompleted from our parent we onComplete children // for each group check if it is ready to accept more events if so pass the oncomplete through else buffer it. for (GroupState group : groups.values()) { @@ -168,9 +175,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { - if (TERMINATED_UPDATER.compareAndSet(this, 0, 1)) { - errorEmitted = true; - + if (TERMINATED_UPDATER.compareAndSet(this, UNTERMINATED, TERMINATED_WITH_ERROR)) { // It's safe to access all groups and emit the error. // onNext and onError are in sequence so no group will be created in the loop. for (GroupState group : groups.values()) { @@ -390,18 +395,16 @@ private void drainIfPossible(GroupState groupState) { } private void completeInner() { + // A group is removed, so check if we need to call `unsubscribe` if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this) == 0) { + // It means `groups.isEmpty() && child.isUnsubscribed()` is true unsubscribe(); } - // if we have no outstanding groups (all completed or unsubscribe) and terminated/unsubscribed on outer - if (groups.isEmpty() && (terminated == 1 || child.isUnsubscribed())) { + // if we have no outstanding groups (all completed or unsubscribe) and terminated on outer + if (groups.isEmpty() && terminated == TERMINATED_WITH_COMPLETED) { // completionEmitted ensures we only emit onCompleted once if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { - - if (child.isUnsubscribed()) { - // if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up. - unsubscribe(); - } else if (!errorEmitted) { + if (!child.isUnsubscribed()) { child.onCompleted(); } } From c148ea5dc8605ebefa5977a211cfcd795bb90dda Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 15 Dec 2014 08:45:30 +0100 Subject: [PATCH 031/857] Fixed test issuing non-serialized messages to the subject. --- .../ReplaySubjectConcurrencyTest.java | 118 ++++++++++-------- 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java index 304533e012..2120a49373 100644 --- a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java @@ -338,63 +338,73 @@ public void run() { public void testReplaySubjectEmissionSubscriptionRace() throws Exception { Scheduler s = Schedulers.io(); Scheduler.Worker worker = Schedulers.io().createWorker(); - for (int i = 0; i < 50000; i++) { - if (i % 1000 == 0) { - System.out.println(i); - } - final ReplaySubject rs = ReplaySubject.create(); - - final CountDownLatch finish = new CountDownLatch(1); - final CountDownLatch start = new CountDownLatch(1); - - worker.schedule(new Action0() { - @Override - public void call() { - try { - start.await(); - } catch (Exception e1) { - e1.printStackTrace(); - } - rs.onNext(1); - } - }); - - final AtomicReference o = new AtomicReference(); - - rs.subscribeOn(s).observeOn(Schedulers.io()) - .subscribe(new Observer() { - - @Override - public void onCompleted() { - o.set(-1); - finish.countDown(); - } - - @Override - public void onError(Throwable e) { - o.set(e); - finish.countDown(); - } - - @Override - public void onNext(Object t) { - o.set(t); - finish.countDown(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); } + final ReplaySubject rs = ReplaySubject.create(); - }); - start.countDown(); - - if (!finish.await(5, TimeUnit.SECONDS)) { - System.out.println(o.get()); - System.out.println(rs.hasObservers()); - rs.onCompleted(); - Assert.fail("Timeout @ " + i); - break; - } else { - Assert.assertEquals(1, o.get()); - rs.onCompleted(); + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Action0() { + @Override + public void call() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference o = new AtomicReference(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new Observer() { + + @Override + public void onCompleted() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onCompleted(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Action0() { + @Override + public void call() { + rs.onCompleted(); + } + }); + + } } + } finally { + worker.unsubscribe(); } } } From da987ed17e9b0dae68d382ec04e616a55e6e4ce2 Mon Sep 17 00:00:00 2001 From: David Gross Date: Mon, 15 Dec 2014 12:03:48 -0800 Subject: [PATCH 032/857] javadocs: minor style, grammar changes; add @since annotation to new method --- .../java/rx/subscriptions/Subscriptions.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/subscriptions/Subscriptions.java b/src/main/java/rx/subscriptions/Subscriptions.java index e94418106f..0fe84240ce 100644 --- a/src/main/java/rx/subscriptions/Subscriptions.java +++ b/src/main/java/rx/subscriptions/Subscriptions.java @@ -31,9 +31,9 @@ private Subscriptions() { throw new IllegalStateException("No instances!"); } /** - * Returns a {@link Subscription} that unsubscribe does nothing except changing - * isUnsubscribed to true. It's stateful and isUnsubscribed - * indicates if unsubscribe is called, which is different from {@link #unsubscribed()}. + * Returns a {@link Subscription} to which {@code unsubscribe} does nothing except to change + * {@code isUnsubscribed} to {@code true}. It's stateful and {@code isUnsubscribed} indicates if + * {@code unsubscribe} is called, which is different from {@link #unsubscribed()}. * *

      * Subscription empty = Subscriptions.empty();
@@ -42,23 +42,24 @@ private Subscriptions() {
      * System.out.println(empty.isUnsubscribed()); // true
      * 
* - * @return a {@link Subscription} that unsubscribe does nothing except changing - * isUnsubscribed to true. + * @return a {@link Subscription} to which {@code unsubscribe} does nothing except to change + * {@code isUnsubscribed} to {@code true} */ public static Subscription empty() { return BooleanSubscription.create(); } /** - * Returns a {@link Subscription} that unsubscribe does nothing but is already unsubscribed. - * Its isUnsubscribed always return true, which is different from {@link #empty()}. + * Returns a {@link Subscription} to which {@code unsubscribe} does nothing, as it is already unsubscribed. + * Its {@code isUnsubscribed} always returns {@code true}, which is different from {@link #empty()}. * *

      * Subscription unsubscribed = Subscriptions.unsubscribed();
      * System.out.println(unsubscribed.isUnsubscribed()); // true
      * 
* - * @return a {@link Subscription} that unsubscribe does nothing but is already unsubscribed. + * @return a {@link Subscription} to which {@code unsubscribe} does nothing, as it is already unsubscribed + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public static Subscription unsubscribed() { From 31281b60e56032ba1d7c99b9411132756f9c2844 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Tue, 16 Dec 2014 10:30:12 +0800 Subject: [PATCH 033/857] Update as per review --- src/main/java/rx/internal/operators/OperatorGroupBy.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index b44e46bb7c..722a047884 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -399,14 +399,11 @@ private void completeInner() { if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this) == 0) { // It means `groups.isEmpty() && child.isUnsubscribed()` is true unsubscribe(); - } - // if we have no outstanding groups (all completed or unsubscribe) and terminated on outer - if (groups.isEmpty() && terminated == TERMINATED_WITH_COMPLETED) { + } else if (groups.isEmpty() && terminated == TERMINATED_WITH_COMPLETED) { + // if we have no outstanding groups (all completed or unsubscribe) and terminated on outer // completionEmitted ensures we only emit onCompleted once if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { - if (!child.isUnsubscribed()) { - child.onCompleted(); - } + child.onCompleted(); } } } From 5c0be524b263476d714c6165a6171a35eb76a5f6 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 15 Dec 2014 20:40:52 -0800 Subject: [PATCH 034/857] Version 1.0.3 --- CHANGES.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 3ffc22a9a6..c1fdb48ded 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,27 @@ # RxJava Releases # +### Version 1.0.3 – December 15th 2014 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.3%7C)) ### + +* [Pull 1928] (https://github.com/ReactiveX/RxJava/pull/1928) Experimental: Add onBackpressureBuffer with capacity +* [Pull 1946] (https://github.com/ReactiveX/RxJava/pull/1946) Experimental: AbstractOnSubscribe to help build Observables one onNext at a time. +* [Pull 1960] (https://github.com/ReactiveX/RxJava/pull/1960) Beta: doOnRequest +* [Pull 1965] (https://github.com/ReactiveX/RxJava/pull/1965) Fix the issue that Sample doesn't call 'unsubscribe' +* [Pull 1966] (https://github.com/ReactiveX/RxJava/pull/1966) Fix NPE when the key is null in GroupBy +* [Pull 1964] (https://github.com/ReactiveX/RxJava/pull/1964) Handle 0 or negative request in Buffer +* [Pull 1957] (https://github.com/ReactiveX/RxJava/pull/1957) Fix 'request(0)' issue in Scan +* [Pull 1950] (https://github.com/ReactiveX/RxJava/pull/1950) Add "Subscriptions.unsubscribed" to fix the 'isUnsubscribed' issue +* [Pull 1938] (https://github.com/ReactiveX/RxJava/pull/1938) Any/All should not unsubscribe downstream. +* [Pull 1968] (https://github.com/ReactiveX/RxJava/pull/1968) Upgrade to Gradle 2.2 +* [Pull 1961] (https://github.com/ReactiveX/RxJava/pull/1961) Remove Request Batching in Merge + +* [Pull 1953] (https://github.com/ReactiveX/RxJava/pull/1953) Fixed timer cast-to-int crash causing incorrect benchmark. +* [Pull 1952] (https://github.com/ReactiveX/RxJava/pull/1952) Remove ActionSubscription +* [Pull 1951] (https://github.com/ReactiveX/RxJava/pull/1951) Remove extraneous request(n) and onCompleted() calls when unsubscribed. +* [Pull 1947] (https://github.com/ReactiveX/RxJava/pull/1947) Fixed first emission racing with pre and post subscription. +* [Pull 1937] (https://github.com/ReactiveX/RxJava/pull/1937) Scheduler.Worker to be finally unsubscribed to avoid interference +* [Pull 1926] (https://github.com/ReactiveX/RxJava/pull/1926) Move the codes out of the finally block +* [Pull 1922] (https://github.com/ReactiveX/RxJava/pull/1922) Set removeOnCancelPolicy on the threadpool if supported + ### Version 1.0.2 – December 1st 2014 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.2%7C)) ### This release adds `@Beta` and `@Experimental` annotations to mark APIs that are not yet stable. From 093bbeb69b77b14b5db39f7f32a4637cf75ae57a Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 18 Dec 2014 17:26:14 +1100 Subject: [PATCH 035/857] OperatorDoOnRequest.ParentSubscriber should be static class --- src/main/java/rx/internal/operators/OperatorDoOnRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java index f31484966d..d77cc21dba 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java @@ -52,7 +52,7 @@ public void request(long n) { return parent; } - private final class ParentSubscriber extends Subscriber { + private static final class ParentSubscriber extends Subscriber { private final Subscriber child; private ParentSubscriber(Subscriber child) { From 1a068ddfd2b9fbadcadb2b49ef20fc93782edc3a Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 18 Dec 2014 08:53:07 +0100 Subject: [PATCH 036/857] Fixed wrong subject use in test. --- .../rx/subjects/ReplaySubjectBoundedConcurrencyTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java index bf4c2bbc1e..9924a8b71e 100644 --- a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java @@ -396,7 +396,12 @@ public void onNext(Object t) { break; } else { Assert.assertEquals(1, o.get()); - rs.onCompleted(); + worker.schedule(new Action0() { + @Override + public void call() { + rs.onCompleted(); + } + }); } } } From a82eff91bebb55fce4fa32225e327170a0ad9246 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 18 Dec 2014 09:29:52 +0100 Subject: [PATCH 037/857] Fixed potential request value overflow. --- .../java/rx/internal/util/BackpressureDrainManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/util/BackpressureDrainManager.java b/src/main/java/rx/internal/util/BackpressureDrainManager.java index 78a4d29c3e..f898805e33 100644 --- a/src/main/java/rx/internal/util/BackpressureDrainManager.java +++ b/src/main/java/rx/internal/util/BackpressureDrainManager.java @@ -142,14 +142,17 @@ public final void request(long n) { r = requestedCount; mayDrain = r == 0; if (r == Long.MAX_VALUE) { - mayDrain = true; break; } if (n == Long.MAX_VALUE) { u = n; mayDrain = true; } else { - u = r + n; + if (r > Long.MAX_VALUE - n) { + u = Long.MAX_VALUE; + } else { + u = r + n; + } } } while (!REQUESTED_COUNT.compareAndSet(this, r, u)); // since we implement producer, we have to call drain From fc7c1713893a33c6775af10b79d7faff853f9976 Mon Sep 17 00:00:00 2001 From: Alex Wenckus Date: Fri, 19 Dec 2014 15:10:03 -0600 Subject: [PATCH 038/857] Added Operator switchIfEmpty, like defaultIfEmpty but subscribes to and emits the items in an Observable if the source is empty --- src/main/java/rx/Observable.java | 18 +++ .../operators/OperatorSwitchIfEmpty.java | 114 +++++++++++++++++ .../operators/OperatorSwitchIfEmptyTest.java | 118 ++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java create mode 100644 src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 6162b1339c..a245a02e38 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3736,6 +3736,24 @@ public final Observable defaultIfEmpty(T defaultValue) { return lift(new OperatorDefaultIfEmpty(defaultValue)); } + /** + * Returns an Observable that emits the items emitted by the source Observable or the items of an alternate Observable if the source Observable + * is empty. + *

+ *

+ *
Scheduler:
+ *
{@code switchIfEmpty} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param alternate + * the alternate Observable to subscribe to if the source does not emit any items + * @return an Observable that emits the items emitted by the source Observable or the items of an alternate Observable if the source Observable + * is empty. + */ + public final Observable switchIfEmpty(Observable alternate) { + return lift(new OperatorSwitchIfEmpty(alternate)); + } + /** * Returns an Observable that delays the subscription to and emissions from the souce Observable via another * Observable on a per-item basis. diff --git a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java new file mode 100644 index 0000000000..53c8556e20 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java @@ -0,0 +1,114 @@ +/** + * 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.Producer; +import rx.Subscriber; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * If the Observable completes without emitting any items, subscribe to an alternate Observable. Allows for similar + * functionality to {@link rx.internal.operators.OperatorDefaultIfEmpty} except instead of one item being emitted when + * empty, the results of the given Observable will be emitted. + */ +public class OperatorSwitchIfEmpty implements Observable.Operator { + private final Observable alternate; + + public OperatorSwitchIfEmpty(Observable alternate) { + this.alternate = alternate; + } + + @Override + public Subscriber call(Subscriber child) { + return new SwitchIfEmptySubscriber(child); + } + + private class SwitchIfEmptySubscriber extends Subscriber { + + boolean empty = true; + final AtomicLong consumerCapacity = new AtomicLong(0l); + + private final Subscriber child; + + public SwitchIfEmptySubscriber(Subscriber child) { + super(child); + + this.child = child; + } + + @Override + public void setProducer(final Producer producer) { + super.setProducer(new Producer() { + @Override + public void request(long n) { + if (empty) { + consumerCapacity.set(n); + } + producer.request(n); + } + }); + } + + @Override + public void onCompleted() { + if (!empty) { + child.onCompleted(); + } else if (!child.isUnsubscribed()) { + subscribeToAlternate(); + } + } + + private void subscribeToAlternate() { + add(alternate.unsafeSubscribe(new Subscriber() { + @Override + public void onStart() { + final long capacity = consumerCapacity.get(); + if (capacity > 0) { + request(capacity); + } + } + + @Override + public void onCompleted() { + child.onCompleted(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + child.onNext(t); + } + })); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + empty = false; + child.onNext(t); + } + } +} diff --git a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java new file mode 100644 index 0000000000..63f6598a6b --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java @@ -0,0 +1,118 @@ +/** + * 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 org.junit.Test; +import rx.Observable; +import rx.Producer; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.schedulers.Schedulers; +import rx.subscriptions.Subscriptions; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class OperatorSwitchIfEmptyTest { + + @Test + public void testSwitchWhenNotEmpty() throws Exception { + final AtomicBoolean subscribed = new AtomicBoolean(false); + final Observable observable = Observable.just(4).switchIfEmpty(Observable.just(2) + .doOnSubscribe(new Action0() { + @Override + public void call() { + subscribed.set(true); + } + })); + + assertEquals(4, observable.toBlocking().single().intValue()); + assertFalse(subscribed.get()); + } + + @Test + public void testSwitchWhenEmpty() throws Exception { + final Observable observable = Observable.empty().switchIfEmpty(Observable.from(Arrays.asList(42))); + + assertEquals(42, observable.toBlocking().single().intValue()); + } + + @Test + public void testSwitchWithProducer() throws Exception { + Observable withProducer = Observable.create(new Observable.OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + subscriber.setProducer(new Producer() { + @Override + public void request(long n) { + if (n > 0) { + subscriber.onNext(42L); + subscriber.onCompleted(); + } + } + }); + } + }); + + final Observable observable = Observable.empty().switchIfEmpty(withProducer); + assertEquals(42, observable.toBlocking().single().intValue()); + } + + @Test + public void testSwitchTriggerUnsubscribe() throws Exception { + final Subscription empty = Subscriptions.empty(); + + Observable withProducer = Observable.create(new Observable.OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + subscriber.add(empty); + subscriber.onNext(42L); + } + }); + + final Subscription sub = Observable.empty().switchIfEmpty(withProducer).lift(new Observable.Operator() { + @Override + public Subscriber call(final Subscriber child) { + return new Subscriber(child) { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Long aLong) { + unsubscribe(); + } + }; + } + }).subscribe(); + + + assertTrue(empty.isUnsubscribed()); + assertTrue(sub.isUnsubscribed()); + } +} \ No newline at end of file From 6e6c771b665ef50b27bef757e692ca7c2b8e20f9 Mon Sep 17 00:00:00 2001 From: Alex Wenckus Date: Sat, 20 Dec 2014 23:39:03 -0600 Subject: [PATCH 039/857] Addressing review feedback: Ensure that we unsubscribe upstream "parent" when switching to alternate. That way upstream will trigger unsubscribe when the first Observable completes. Added test. Child should contain downstream subscriptions - not parent. --- src/main/java/rx/Observable.java | 5 ++++- .../operators/OperatorSwitchIfEmpty.java | 9 +++++---- .../operators/OperatorSwitchIfEmptyTest.java | 16 ++++++++++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index a245a02e38..5aac1e53c1 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3739,10 +3739,12 @@ public final Observable defaultIfEmpty(T defaultValue) { /** * Returns an Observable that emits the items emitted by the source Observable or the items of an alternate Observable if the source Observable * is empty. - *

+ *

*

*
Scheduler:
*
{@code switchIfEmpty} does not operate by default on a particular {@link Scheduler}.
+ *
Beta:
+ *
{@code switchIfEmpty} is currently in {@link rx.annotations.Beta} and subject to change.
*
* * @param alternate @@ -3750,6 +3752,7 @@ public final Observable defaultIfEmpty(T defaultValue) { * @return an Observable that emits the items emitted by the source Observable or the items of an alternate Observable if the source Observable * is empty. */ + @Beta public final Observable switchIfEmpty(Observable alternate) { return lift(new OperatorSwitchIfEmpty(alternate)); } diff --git a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java index 53c8556e20..67fafbd8d2 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java +++ b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java @@ -35,7 +35,9 @@ public OperatorSwitchIfEmpty(Observable alternate) { @Override public Subscriber call(Subscriber child) { - return new SwitchIfEmptySubscriber(child); + final SwitchIfEmptySubscriber parent = new SwitchIfEmptySubscriber(child); + child.add(parent); + return parent; } private class SwitchIfEmptySubscriber extends Subscriber { @@ -46,8 +48,6 @@ private class SwitchIfEmptySubscriber extends Subscriber { private final Subscriber child; public SwitchIfEmptySubscriber(Subscriber child) { - super(child); - this.child = child; } @@ -69,12 +69,13 @@ public void onCompleted() { if (!empty) { child.onCompleted(); } else if (!child.isUnsubscribed()) { + unsubscribe(); subscribeToAlternate(); } } private void subscribeToAlternate() { - add(alternate.unsafeSubscribe(new Subscriber() { + child.add(alternate.unsafeSubscribe(new Subscriber() { @Override public void onStart() { final long capacity = consumerCapacity.get(); diff --git a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java index 63f6598a6b..443953563d 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java @@ -21,8 +21,6 @@ import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; -import rx.functions.Action1; -import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; import java.util.Arrays; @@ -115,4 +113,18 @@ public void onNext(Long aLong) { assertTrue(empty.isUnsubscribed()); assertTrue(sub.isUnsubscribed()); } + + @Test + public void testSwitchShouldTriggerUnsubscribe() { + final Subscription s = Subscriptions.empty(); + + Observable.create(new Observable.OnSubscribe() { + @Override + public void call(final Subscriber subscriber) { + subscriber.add(s); + subscriber.onCompleted(); + } + }).switchIfEmpty(Observable.never()).subscribe(); + assertTrue(s.isUnsubscribed()); + } } \ No newline at end of file From f2a59c6ea1b336cf2b7563fccbeecf278777808a Mon Sep 17 00:00:00 2001 From: Alex Wenckus Date: Mon, 22 Dec 2014 00:44:17 -0600 Subject: [PATCH 040/857] Fix for back pressure on the alternate subscription. --- .../operators/OperatorSwitchIfEmpty.java | 11 ++++++ .../operators/OperatorSwitchIfEmptyTest.java | 35 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java index 67fafbd8d2..615594cc17 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java +++ b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java @@ -76,6 +76,17 @@ public void onCompleted() { private void subscribeToAlternate() { child.add(alternate.unsafeSubscribe(new Subscriber() { + + @Override + public void setProducer(final Producer producer) { + child.setProducer(new Producer() { + @Override + public void request(long n) { + producer.request(n); + } + }); + } + @Override public void onStart() { final long capacity = consumerCapacity.get(); diff --git a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java index 443953563d..3fc735ccab 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java @@ -23,7 +23,9 @@ import rx.functions.Action0; import rx.subscriptions.Subscriptions; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; @@ -56,13 +58,15 @@ public void testSwitchWhenEmpty() throws Exception { @Test public void testSwitchWithProducer() throws Exception { + final AtomicBoolean emitted = new AtomicBoolean(false); Observable withProducer = Observable.create(new Observable.OnSubscribe() { @Override public void call(final Subscriber subscriber) { subscriber.setProducer(new Producer() { @Override public void request(long n) { - if (n > 0) { + if (n > 0 && !emitted.get()) { + emitted.set(true); subscriber.onNext(42L); subscriber.onCompleted(); } @@ -127,4 +131,33 @@ public void call(final Subscriber subscriber) { }).switchIfEmpty(Observable.never()).subscribe(); assertTrue(s.isUnsubscribed()); } + + @Test + public void testSwitchRequestAlternativeObservableWithBackpressure() { + final List items = new ArrayList(); + + Observable.empty().switchIfEmpty(Observable.just(1, 2, 3)).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer integer) { + items.add(integer); + } + }); + assertEquals(Arrays.asList(1), items); + } } \ No newline at end of file From 3563021690b04d18c2419d8b2c2f1cf8b5065be4 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 22 Dec 2014 16:07:43 +0800 Subject: [PATCH 041/857] Fix the issue that map may swallow fatal exceptions --- .../rx/internal/operators/OperatorMap.java | 2 ++ .../internal/operators/OperatorMapTest.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/rx/internal/operators/OperatorMap.java b/src/main/java/rx/internal/operators/OperatorMap.java index 1a34918440..1f82a21764 100644 --- a/src/main/java/rx/internal/operators/OperatorMap.java +++ b/src/main/java/rx/internal/operators/OperatorMap.java @@ -17,6 +17,7 @@ import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; @@ -53,6 +54,7 @@ public void onNext(T t) { try { o.onNext(transformer.call(t)); } catch (Throwable e) { + Exceptions.throwIfFatal(e); onError(OnErrorThrowable.addValueAsLastCause(e, t)); } } diff --git a/src/test/java/rx/internal/operators/OperatorMapTest.java b/src/test/java/rx/internal/operators/OperatorMapTest.java index a1de9588af..bcca50fab8 100644 --- a/src/test/java/rx/internal/operators/OperatorMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorMapTest.java @@ -319,4 +319,24 @@ private static Map getMap(String prefix) { m.put("lastName", prefix + "Last"); return m; } + + @Test(expected = OnErrorNotImplementedException.class) + public void testShouldNotSwallowOnErrorNotImplementedException() { + Observable.just("a", "b").flatMap(new Func1>() { + @Override + public Observable call(String s) { + return Observable.just(s + "1", s + "2"); + } + }).flatMap(new Func1>() { + @Override + public Observable call(String s) { + return Observable.error(new Exception("test")); + } + }).forEach(new Action1() { + @Override + public void call(String s) { + System.out.println(s); + } + }); + } } From a683fccd1e2ee5b7cfabbda473a885f2521ac26b Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 23 Dec 2014 09:43:02 -0800 Subject: [PATCH 042/857] enable faster travis builds --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 96a286564f..c228c30b5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,6 @@ language: java jdk: - oraclejdk7 + +sudo: false +# as per http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ From e39599cc4fc01864d6aa1297b85c414d53bc97a2 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 23 Dec 2014 09:38:37 -0800 Subject: [PATCH 043/857] Make Publish Operator Release RingBuffer - it was retaining the RxRingBuffer reference between subscribes which meant it was never released to the object pool --- .../internal/operators/OperatorPublish.java | 30 +++++++++++++------ .../operators/OnSubscribeRefCountTest.java | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 3b04da4058..6e2328e012 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -99,7 +99,7 @@ public void call() { public void connect(Action1 connection) { // each time we connect we create a new Subscription boolean shouldSubscribe = false; - + // subscription is the state of whether we are connected or not OriginSubscriber origin = requestHandler.state.getOrigin(); if (origin == null) { @@ -113,7 +113,7 @@ public void connect(Action1 connection) { connection.call(Subscriptions.create(new Action0() { @Override public void call() { - Subscription s = requestHandler.state.getOrigin(); + OriginSubscriber s = requestHandler.state.getOrigin(); requestHandler.state.setOrigin(null); if (s != null) { s.unsubscribe(); @@ -135,9 +135,11 @@ private static class OriginSubscriber extends Subscriber { private final RequestHandler requestHandler; private final AtomicLong originOutstanding = new AtomicLong(); private final long THRESHOLD = RxRingBuffer.SIZE / 4; + private final RxRingBuffer buffer = RxRingBuffer.getSpmcInstance(); OriginSubscriber(RequestHandler requestHandler) { this.requestHandler = requestHandler; + add(buffer); } @Override @@ -199,6 +201,8 @@ public void onNext(T t) { * with a complicated state machine so I'm sticking with mutex locks and just trying to make sure the work done while holding the * lock is small (such as never emitting data). * + * This does however mean we can't rely on a reference to State being consistent. For example, it can end up with a null OriginSubscriber. + * * @param */ private static class State { @@ -288,7 +292,7 @@ private long resetAfterSubscriberUpdate() { private static class RequestHandler { private final NotificationLite notifier = NotificationLite.instance(); - private final RxRingBuffer buffer = RxRingBuffer.getSpmcInstance(); + private final State state = new State(); @SuppressWarnings("unused") volatile long wip; @@ -297,16 +301,24 @@ private static class RequestHandler { public void requestFromChildSubscriber(Subscriber subscriber, Long request) { state.requestFromSubscriber(subscriber, request); - drainQueue(); + OriginSubscriber originSubscriber = state.getOrigin(); + if(originSubscriber != null) { + drainQueue(originSubscriber); + } } public void emit(Object t) throws MissingBackpressureException { + OriginSubscriber originSubscriber = state.getOrigin(); + if(originSubscriber == null) { + // unsubscribed so break ... we are done + return; + } if (notifier.isCompleted(t)) { - buffer.onCompleted(); + originSubscriber.buffer.onCompleted(); } else { - buffer.onNext(notifier.getValue(t)); + originSubscriber.buffer.onNext(notifier.getValue(t)); } - drainQueue(); + drainQueue(originSubscriber); } private void requestMoreAfterEmission(int emitted) { @@ -319,7 +331,7 @@ private void requestMoreAfterEmission(int emitted) { } } - public void drainQueue() { + public void drainQueue(OriginSubscriber originSubscriber) { if (WIP.getAndIncrement(this) == 0) { int emitted = 0; do { @@ -338,7 +350,7 @@ public void drainQueue() { if (!shouldEmit) { break; } - Object o = buffer.poll(); + Object o = originSubscriber.buffer.poll(); if (o == null) { // nothing in buffer so increment outstanding back again state.incrementOutstandingAfterFailedEmit(); diff --git a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java index d9b8364071..bdd6f47e3e 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java @@ -154,7 +154,7 @@ public void call(Integer l) { s2.unsubscribe(); // unsubscribe s2 first as we're counting in 1 and there can be a race between unsubscribe and one subscriber getting a value but not the other s1.unsubscribe(); - System.out.println("onNext: " + nextCount.get()); + System.out.println("onNext Count: " + nextCount.get()); // it will emit twice because it is synchronous assertEquals(nextCount.get(), receivedCount.get() * 2); From e26dd2bc72f09b7ce59ca9d646bbe1b18b82782d Mon Sep 17 00:00:00 2001 From: zsxwing Date: Wed, 24 Dec 2014 15:58:24 +0800 Subject: [PATCH 044/857] Fix the bug that cache doesn't unsubscribe the source Observable when the source is terminated --- .../rx/internal/operators/OnSubscribeCache.java | 5 +++-- .../internal/operators/OnSubscribeCacheTest.java | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeCache.java b/src/main/java/rx/internal/operators/OnSubscribeCache.java index 4200ac273c..af54fc9f90 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCache.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCache.java @@ -64,9 +64,10 @@ public OnSubscribeCache(Observable source, int capacity) { @Override public void call(Subscriber s) { if (SRC_SUBSCRIBED_UPDATER.compareAndSet(this, 0, 1)) { - source.unsafeSubscribe(Subscribers.from(cache)); + source.subscribe(cache); /* - * Note that we will never unsubscribe from 'source' as we want to receive and cache all of its values. + * Note that we will never unsubscribe from 'source' unless we receive `onCompleted` or `onError`, + * as we want to receive and cache all of its values. * * This means this should never be used on an infinite or very large sequence, similar to toList(). */ diff --git a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java index 7f602826a3..3303106d93 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java @@ -17,6 +17,9 @@ 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 java.util.Arrays; import java.util.concurrent.CountDownLatch; @@ -27,10 +30,10 @@ import rx.Observable; import rx.Subscriber; +import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; -import rx.internal.operators.OnSubscribeCache; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.subjects.AsyncSubject; @@ -148,4 +151,14 @@ public void testWithPublishSubjectAndRepeat() { public void testWithReplaySubjectAndRepeat() { testWithCustomSubjectAndRepeat(ReplaySubject. create(), 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3); } + + @Test + public void testUnsubscribeSource() { + Action0 unsubscribe = mock(Action0.class); + Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, times(1)).call(); + } } From 36b323e41f4437c2dbd24d3cf6dfeea730c4c953 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 25 Dec 2014 06:11:05 +1100 Subject: [PATCH 045/857] add check for isUnsubscribed to OperatorTakeLast fast path --- .../operators/TakeLastQueueProducer.java | 2 ++ .../operators/OperatorTakeLastTest.java | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java index b231dc900a..041242163d 100644 --- a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java +++ b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java @@ -70,6 +70,8 @@ void emit(long previousRequested) { if (previousRequested == 0) { try { for (Object value : deque) { + if (subscriber.isUnsubscribed()) + return; notification.accept(subscriber, value); } } catch (Throwable e) { diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java index 0bdc422f02..c2b1ef014c 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.mockito.InOrder; @@ -264,4 +265,32 @@ public void onNext(Integer integer) { } }); } + + @Test + public void testUnsubscribeTakesEffectEarlyOnFastPath() { + final AtomicInteger count = new AtomicInteger(); + Observable.range(0, 100000).takeLast(100000).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer integer) { + count.incrementAndGet(); + unsubscribe(); + } + }); + assertEquals(1,count.get()); + } } From 1a94d55fa8896931175896d09b86dca8d8d44f72 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 29 Dec 2014 11:53:36 -0800 Subject: [PATCH 046/857] Version 1.0.4 --- CHANGES.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index c1fdb48ded..28067fd11b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # RxJava Releases # +### Version 1.0.4 – December 29th 2014 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.4%7C)) ### + +* [Pull 2156] (https://github.com/ReactiveX/RxJava/pull/2156) Fix the issue that map may swallow fatal exceptions +* [Pull 1967] (https://github.com/ReactiveX/RxJava/pull/1967) Fix the issue that GroupBy may not call 'unsubscribe' +* [Pull 2052] (https://github.com/ReactiveX/RxJava/pull/2052) OperatorDoOnRequest.ParentSubscriber should be static class +* [Pull 2237] (https://github.com/ReactiveX/RxJava/pull/2237) Make Publish Operator Release RingBuffer +* [Pull 2053] (https://github.com/ReactiveX/RxJava/pull/2053) Fixed wrong bounded ReplaySubject use in test + ### Version 1.0.3 – December 15th 2014 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.3%7C)) ### * [Pull 1928] (https://github.com/ReactiveX/RxJava/pull/1928) Experimental: Add onBackpressureBuffer with capacity From 7563d8009f806962d3b48fa10d311f700903221f Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 5 Jan 2015 23:36:05 +0100 Subject: [PATCH 047/857] Few adjustments. --- .../internal/util/unsafe/SpscArrayQueue.java | 11 +-- .../java/rx/observers/TestSubscriber.java | 4 +- .../internal/operators/OperatorRetryTest.java | 88 +++++++++++++++---- 3 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java index f9de0a7b74..16d1e81fae 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java @@ -123,18 +123,19 @@ public boolean offer(final E e) { } // local load of field to avoid repeated loads after volatile reads final E[] lElementBuffer = buffer; + final long offset = calcElementOffset(producerIndex); if (producerIndex >= producerLookAhead) { - if (null != lvElement(lElementBuffer, calcElementOffset(producerIndex + lookAheadStep))) {// LoadLoad + if (null == lvElement(lElementBuffer, calcElementOffset(producerIndex + lookAheadStep))) {// LoadLoad + producerLookAhead = producerIndex + lookAheadStep; + } + else if (null != lvElement(lElementBuffer, offset)){ return false; } - producerLookAhead = producerIndex + lookAheadStep; } - long offset = calcElementOffset(producerIndex); producerIndex++; // do increment here so the ordered store give both a barrier soElement(lElementBuffer, offset, e);// StoreStore return true; - } - + } /** * {@inheritDoc} *

diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index c9bef5f898..4131957405 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -227,9 +227,9 @@ public void awaitTerminalEvent() { * @throws RuntimeException * if the Subscriber is interrupted before the Observable is able to complete */ - public void awaitTerminalEvent(long timeout, TimeUnit unit) { + public boolean awaitTerminalEvent(long timeout, TimeUnit unit) { try { - latch.await(timeout, unit); + return latch.await(timeout, unit); } catch (InterruptedException e) { throw new RuntimeException("Interrupted", e); } diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index a5a3f9ef9f..39d9b260ba 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -15,19 +15,33 @@ */ package rx.internal.operators; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.junit.Test; -import org.mockito.*; +import org.mockito.InOrder; +import org.mockito.Mockito; -import rx.*; +import rx.Observable; import rx.Observable.OnSubscribe; -import rx.functions.*; +import rx.Observer; +import rx.Producer; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.functions.Func2; import rx.internal.util.RxRingBuffer; import rx.observables.GroupedObservable; import rx.observers.TestSubscriber; @@ -409,15 +423,20 @@ public void request(long n) { if (i < numFailures) { o.onNext("beginningEveryTime"); o.onError(new RuntimeException("forced failure: " + count.get())); - } else - if (i == numFailures) { - o.onNext("beginningEveryTime"); - } else - if (i > numFailures) { - o.onNext("onSuccessOnly"); - o.onCompleted(); + req.decrementAndGet(); + } else { + do { + if (i == numFailures) { + o.onNext("beginningEveryTime"); + } else + if (i > numFailures) { + o.onNext("onSuccessOnly"); + o.onCompleted(); + break; + } + i = count.getAndIncrement(); + } while (req.decrementAndGet() > 0); } - req.decrementAndGet(); } } }); @@ -675,15 +694,15 @@ public void testTimeoutWithRetry() { } @Test(timeout = 10000) - public void testRetryWithBackpressure() { - for (int i = 0; i < 200; i++) { + public void testRetryWithBackpressure() throws InterruptedException { + final int NUM_RETRIES = RxRingBuffer.SIZE * 2; + for (int i = 0; i < 400; i++) { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); - int NUM_RETRIES = RxRingBuffer.SIZE * 2; Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); TestSubscriber ts = new TestSubscriber(observer); origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); - ts.awaitTerminalEvent(); + ts.awaitTerminalEvent(5, TimeUnit.SECONDS); InOrder inOrder = inOrder(observer); // should have no errors @@ -697,6 +716,39 @@ public void testRetryWithBackpressure() { inOrder.verifyNoMoreInteractions(); } } + @Test(timeout = 10000) + public void testRetryWithBackpressureParallel() throws InterruptedException { + final int NUM_RETRIES = RxRingBuffer.SIZE * 2; + int ncpu = Runtime.getRuntime().availableProcessors(); + ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 1)); + final AtomicInteger timeouts = new AtomicInteger(); + int m = 300; + final CountDownLatch cdl = new CountDownLatch(m); + for (int i = 0; i < m; i++) { + final int j = i; + exec.execute(new Runnable() { + @Override + public void run() { + try { + Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); + TestSubscriber ts = new TestSubscriber(); + origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); + if (!ts.awaitTerminalEvent(10, TimeUnit.SECONDS)) { + timeouts.incrementAndGet(); + System.out.println(j + " | " + cdl.getCount() + " !!!"); + } + } catch (Throwable t) { + timeouts.incrementAndGet(); + } + cdl.countDown(); + } + }); + } + exec.shutdown(); + cdl.await(); + assertEquals(0, timeouts.get()); + + } @Test(timeout = 3000) public void testIssue1900() throws InterruptedException { @SuppressWarnings("unchecked") From 8741a21b8879682676a79a8110d8f8937250ed34 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 6 Jan 2015 00:04:28 +0100 Subject: [PATCH 048/857] Fixed the error function. --- .../internal/operators/OperatorRetryTest.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index 39d9b260ba..4fcafed32c 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -418,7 +418,7 @@ public void request(long n) { } return; } - if (n > 0 && req.getAndAdd(1) == 0) { + if (n > 0 && req.getAndAdd(n) == 0) { int i = count.getAndIncrement(); if (i < numFailures) { o.onNext("beginningEveryTime"); @@ -722,7 +722,8 @@ public void testRetryWithBackpressureParallel() throws InterruptedException { int ncpu = Runtime.getRuntime().availableProcessors(); ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 1)); final AtomicInteger timeouts = new AtomicInteger(); - int m = 300; + final AtomicInteger data = new AtomicInteger(); + int m = 2000; final CountDownLatch cdl = new CountDownLatch(m); for (int i = 0; i < m; i++) { final int j = i; @@ -730,12 +731,22 @@ public void testRetryWithBackpressureParallel() throws InterruptedException { @Override public void run() { try { + final AtomicInteger nexts = new AtomicInteger(); Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); TestSubscriber ts = new TestSubscriber(); origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); - if (!ts.awaitTerminalEvent(10, TimeUnit.SECONDS)) { + if (!ts.awaitTerminalEvent(2, TimeUnit.SECONDS)) { timeouts.incrementAndGet(); - System.out.println(j + " | " + cdl.getCount() + " !!!"); + System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); + } + if (ts.getOnNextEvents().size() != NUM_RETRIES + 2) { + data.incrementAndGet(); + } + if (ts.getOnErrorEvents().size() != 0) { + data.incrementAndGet(); + } + if (ts.getOnCompletedEvents().size() != 1) { + data.incrementAndGet(); } } catch (Throwable t) { timeouts.incrementAndGet(); @@ -747,6 +758,7 @@ public void run() { exec.shutdown(); cdl.await(); assertEquals(0, timeouts.get()); + assertEquals(0, data.get()); } @Test(timeout = 3000) From 553241286bb152945adcd21992f07a243ca5dd70 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 6 Jan 2015 17:06:26 +0100 Subject: [PATCH 049/857] A first set of stateless operators that don't need to be instantiated for lift all the time. --- src/main/java/rx/Observable.java | 14 +++---- .../operators/OperatorAsObservable.java | 13 +++++- .../rx/internal/operators/OperatorConcat.java | 13 ++++++ .../operators/OperatorDematerialize.java | 13 +++++- .../operators/OperatorMaterialize.java | 13 +++++- .../rx/internal/operators/OperatorMerge.java | 42 +++++++++++++++++-- .../operators/OperatorMergeDelayError.java | 42 ------------------- 7 files changed, 94 insertions(+), 56 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OperatorMergeDelayError.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 6162b1339c..b98d2393a0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -758,7 +758,7 @@ public static final Observable combineLatest(ListRxJava wiki: concat */ public final static Observable concat(Observable> observables) { - return observables.lift(new OperatorConcat()); + return observables.lift(OperatorConcat.instance()); } /** @@ -1619,7 +1619,7 @@ public final static Observable merge(IterableRxJava wiki: merge */ public final static Observable merge(Observable> source) { - return source.lift(new OperatorMerge()); + return source.lift(OperatorMerge.instance(false)); } /** @@ -1944,7 +1944,7 @@ public final static Observable merge(Observable[] sequences) * @see RxJava wiki: mergeDelayError */ public final static Observable mergeDelayError(Observable> source) { - return source.lift(new OperatorMergeDelayError()); + return source.lift(OperatorMerge.instance(true)); } /** @@ -3002,7 +3002,7 @@ public final Observable ambWith(Observable t1) { * @return an Observable that hides the identity of this Observable */ public final Observable asObservable() { - return lift(new OperatorAsObservable()); + return lift(OperatorAsObservable.instance()); } /** @@ -3923,9 +3923,9 @@ public final Observable delaySubscription(Func0> * if the source Observable is not of type {@code Observable>} * @see RxJava wiki: dematerialize */ - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({"unchecked"}) public final Observable dematerialize() { - return lift(new OperatorDematerialize()); + return lift(OperatorDematerialize.instance()); } /** @@ -4968,7 +4968,7 @@ private final Observable mapNotification(Func1 on * @see RxJava wiki: materialize */ public final Observable> materialize() { - return lift(new OperatorMaterialize()); + return lift(OperatorMaterialize.instance()); } /** diff --git a/src/main/java/rx/internal/operators/OperatorAsObservable.java b/src/main/java/rx/internal/operators/OperatorAsObservable.java index d8cbc7b756..3f748aeb38 100644 --- a/src/main/java/rx/internal/operators/OperatorAsObservable.java +++ b/src/main/java/rx/internal/operators/OperatorAsObservable.java @@ -25,7 +25,18 @@ * the return value type of the wrapped observable. */ public final class OperatorAsObservable implements Operator { - + /** Lazy initialization via inner-class holder. */ + private static final class Holder { + /** A singleton instance. */ + static final OperatorAsObservable INSTANCE = new OperatorAsObservable(); + } + /** + * @return an singleton instance of this stateless operator. + */ + @SuppressWarnings("unchecked") + public static OperatorAsObservable instance() { + return (OperatorAsObservable)Holder.INSTANCE; + } @Override public Subscriber call(Subscriber s) { return s; diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index 7083d8b770..f4493e7d00 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -37,6 +37,19 @@ * 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 an singleton instance of this stateless operator. + */ + @SuppressWarnings("unchecked") + public static OperatorConcat instance() { + return (OperatorConcat)Holder.INSTANCE; + } + @Override public Subscriber> call(final Subscriber child) { final SerializedSubscriber s = new SerializedSubscriber(child); diff --git a/src/main/java/rx/internal/operators/OperatorDematerialize.java b/src/main/java/rx/internal/operators/OperatorDematerialize.java index 85acf7b5e3..b7e250340b 100644 --- a/src/main/java/rx/internal/operators/OperatorDematerialize.java +++ b/src/main/java/rx/internal/operators/OperatorDematerialize.java @@ -30,7 +30,18 @@ * @param the wrapped value type */ public final class OperatorDematerialize implements Operator> { - + /** Lazy initialization via inner-class holder. */ + private static final class Holder { + /** A singleton instance. */ + static final OperatorDematerialize INSTANCE = new OperatorDematerialize(); + } + /** + * @return an singleton instance of this stateless operator. + */ + @SuppressWarnings({ "rawtypes" }) + public static OperatorDematerialize instance() { + return Holder.INSTANCE; // using raw types because the type inference is not good enough + } @Override public Subscriber> call(final Subscriber child) { return new Subscriber>(child) { diff --git a/src/main/java/rx/internal/operators/OperatorMaterialize.java b/src/main/java/rx/internal/operators/OperatorMaterialize.java index ad1fb5dd09..fdfe179cf6 100644 --- a/src/main/java/rx/internal/operators/OperatorMaterialize.java +++ b/src/main/java/rx/internal/operators/OperatorMaterialize.java @@ -29,7 +29,18 @@ * See here for the Microsoft Rx equivalent. */ public final class OperatorMaterialize implements Operator, T> { - + /** Lazy initialization via inner-class holder. */ + private static final class Holder { + /** A singleton instance. */ + static final OperatorMaterialize INSTANCE = new OperatorMaterialize(); + } + /** + * @return an singleton instance of this stateless operator. + */ + @SuppressWarnings("unchecked") + public static OperatorMaterialize instance() { + return (OperatorMaterialize)Holder.INSTANCE; + } @Override public Subscriber call(final Subscriber> child) { return new Subscriber(child) { diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 297a4412f9..75da96dec7 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -38,12 +38,46 @@ * *

* You can combine the items emitted by multiple {@code Observable}s so that they act like a single {@code Observable}, by using the merge operation. - * + *

+ * The {@code instance(true)} call behaves like {@link OperatorMerge} except that if any of the merged Observables notify of + * an error via {@code onError}, {@code mergeDelayError} will refrain from propagating that error + * notification until all of the merged Observables have finished emitting items. + *

+ * + *

+ * Even if multiple merged Observables send {@code onError} notifications, {@code mergeDelayError} will + * only invoke the {@code onError} method of its Observers once. + *

+ * This operation allows an Observer to receive all successfully emitted items from all of the + * source Observables without being interrupted by an error notification from one of them. + *

+ * Note: If this is used on an Observable that never completes, it will never call {@code onError} and will effectively swallow errors. + * @param * the type of the items emitted by both the source and merged {@code Observable}s */ public class OperatorMerge implements Operator> { - + /** Lazy initialization via inner-class holder. */ + private static final class HolderNoDelay { + /** A singleton instance. */ + static final OperatorMerge INSTANCE = new OperatorMerge(false); + } + /** Lazy initialization via inner-class holder. */ + private static final class HolderDelayErrors { + /** A singleton instance. */ + static final OperatorMerge INSTANCE = new OperatorMerge(true); + } + /** + * @param delayErrors should the merge delay errors? + * @return an singleton instance of this stateless operator. + */ + @SuppressWarnings("unchecked") + public static OperatorMerge instance(boolean delayErrors) { + if (delayErrors) { + return (OperatorMerge)HolderDelayErrors.INSTANCE; + } + return (OperatorMerge)HolderNoDelay.INSTANCE; + } /* * benjchristensen => This class is complex and I'm not a fan of it despite writing it. I want to give some background * as to why for anyone who wants to try and help improve it. @@ -74,11 +108,11 @@ public class OperatorMerge implements Operator> { * to track object allocation. */ - public OperatorMerge() { + private OperatorMerge() { this.delayErrors = false; } - public OperatorMerge(boolean delayErrors) { + private OperatorMerge(boolean delayErrors) { this.delayErrors = delayErrors; } diff --git a/src/main/java/rx/internal/operators/OperatorMergeDelayError.java b/src/main/java/rx/internal/operators/OperatorMergeDelayError.java deleted file mode 100644 index 491847cfaa..0000000000 --- a/src/main/java/rx/internal/operators/OperatorMergeDelayError.java +++ /dev/null @@ -1,42 +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; - -/** - * This behaves like {@link OperatorMerge} except that if any of the merged Observables notify of - * an error via {@code onError}, {@code mergeDelayError} will refrain from propagating that error - * notification until all of the merged Observables have finished emitting items. - *

- * - *

- * Even if multiple merged Observables send {@code onError} notifications, {@code mergeDelayError} will - * only invoke the {@code onError} method of its Observers once. - *

- * This operation allows an Observer to receive all successfully emitted items from all of the - * source Observables without being interrupted by an error notification from one of them. - *

- * Note: If this is used on an Observable that never completes, it will never call {@code onError} and will effectively swallow errors. - * - * @param - * the source and result value type - */ -public final class OperatorMergeDelayError extends OperatorMerge { - - public OperatorMergeDelayError() { - super(true); - } - -} From 824c65a1680e338997bd2babb8d1cace64d5aaec Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 7 Jan 2015 08:37:49 +0100 Subject: [PATCH 050/857] Fixed comment typo, added the rest of the operators --- src/main/java/rx/Observable.java | 8 ++++---- .../internal/operators/OperatorAsObservable.java | 3 ++- .../rx/internal/operators/OperatorConcat.java | 4 ++-- .../internal/operators/OperatorDematerialize.java | 3 ++- .../internal/operators/OperatorMaterialize.java | 3 ++- .../java/rx/internal/operators/OperatorMerge.java | 2 +- .../operators/OperatorOnBackpressureDrop.java | 14 +++++++++++++- .../rx/internal/operators/OperatorSerialize.java | 14 +++++++++++++- .../rx/internal/operators/OperatorSwitch.java | 15 ++++++++++++++- .../operators/OperatorToObservableList.java | 14 +++++++++++++- .../operators/OperatorToObservableListTest.java | 7 +++---- 11 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index b98d2393a0..73183c206f 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -2419,7 +2419,7 @@ public final static Observable sequenceEqual(ObservableRxJava wiki: switchOnNext */ public final static Observable switchOnNext(Observable> sequenceOfSequences) { - return sequenceOfSequences.lift(new OperatorSwitch()); + return sequenceOfSequences.lift(OperatorSwitch.instance()); } /** @@ -5113,7 +5113,7 @@ public final Observable onBackpressureBuffer(long capacity, Action0 onOverflo * @see RxJava wiki: Backpressure */ public final Observable onBackpressureDrop() { - return lift(new OperatorOnBackpressureDrop()); + return lift(OperatorOnBackpressureDrop.instance()); } /** @@ -6463,7 +6463,7 @@ public final Observable scan(R initialValue, Func2 accum * @see RxJava wiki: serialize */ public final Observable serialize() { - return lift(new OperatorSerialize()); + return lift(OperatorSerialize.instance()); } /** @@ -8370,7 +8370,7 @@ public final BlockingObservable toBlocking() { * @see RxJava wiki: toList */ public final Observable> toList() { - return lift(new OperatorToObservableList()); + return lift(OperatorToObservableList.instance()); } /** diff --git a/src/main/java/rx/internal/operators/OperatorAsObservable.java b/src/main/java/rx/internal/operators/OperatorAsObservable.java index 3f748aeb38..41f2cb3ebc 100644 --- a/src/main/java/rx/internal/operators/OperatorAsObservable.java +++ b/src/main/java/rx/internal/operators/OperatorAsObservable.java @@ -31,12 +31,13 @@ private static final class Holder { static final OperatorAsObservable INSTANCE = new OperatorAsObservable(); } /** - * @return an singleton instance of this stateless operator. + * @return a singleton instance of this stateless operator. */ @SuppressWarnings("unchecked") public static OperatorAsObservable instance() { return (OperatorAsObservable)Holder.INSTANCE; } + private OperatorAsObservable() { } @Override public Subscriber call(Subscriber s) { return s; diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index f4493e7d00..dc06b7561e 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -43,13 +43,13 @@ private static final class Holder { static final OperatorConcat INSTANCE = new OperatorConcat(); } /** - * @return an singleton instance of this stateless operator. + * @return a singleton instance of this stateless operator. */ @SuppressWarnings("unchecked") public static OperatorConcat instance() { return (OperatorConcat)Holder.INSTANCE; } - + private OperatorConcat() { } @Override public Subscriber> call(final Subscriber child) { final SerializedSubscriber s = new SerializedSubscriber(child); diff --git a/src/main/java/rx/internal/operators/OperatorDematerialize.java b/src/main/java/rx/internal/operators/OperatorDematerialize.java index b7e250340b..d9a154d795 100644 --- a/src/main/java/rx/internal/operators/OperatorDematerialize.java +++ b/src/main/java/rx/internal/operators/OperatorDematerialize.java @@ -36,12 +36,13 @@ private static final class Holder { static final OperatorDematerialize INSTANCE = new OperatorDematerialize(); } /** - * @return an singleton instance of this stateless operator. + * @return a singleton instance of this stateless operator. */ @SuppressWarnings({ "rawtypes" }) public static OperatorDematerialize instance() { return Holder.INSTANCE; // using raw types because the type inference is not good enough } + private OperatorDematerialize() { } @Override public Subscriber> call(final Subscriber child) { return new Subscriber>(child) { diff --git a/src/main/java/rx/internal/operators/OperatorMaterialize.java b/src/main/java/rx/internal/operators/OperatorMaterialize.java index fdfe179cf6..bd5771747c 100644 --- a/src/main/java/rx/internal/operators/OperatorMaterialize.java +++ b/src/main/java/rx/internal/operators/OperatorMaterialize.java @@ -35,12 +35,13 @@ private static final class Holder { static final OperatorMaterialize INSTANCE = new OperatorMaterialize(); } /** - * @return an singleton instance of this stateless operator. + * @return a singleton instance of this stateless operator. */ @SuppressWarnings("unchecked") public static OperatorMaterialize instance() { return (OperatorMaterialize)Holder.INSTANCE; } + private OperatorMaterialize() { } @Override public Subscriber call(final Subscriber> child) { return new Subscriber(child) { diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 75da96dec7..269135446a 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -69,7 +69,7 @@ private static final class HolderDelayErrors { } /** * @param delayErrors should the merge delay errors? - * @return an singleton instance of this stateless operator. + * @return a singleton instance of this stateless operator. */ @SuppressWarnings("unchecked") public static OperatorMerge instance(boolean delayErrors) { diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java index eb595dae0a..ef92ccd542 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java @@ -22,7 +22,19 @@ import rx.Subscriber; public class OperatorOnBackpressureDrop implements Operator { - + /** Lazy initialization via inner-class holder. */ + private static final class Holder { + /** A singleton instance. */ + static final OperatorOnBackpressureDrop INSTANCE = new OperatorOnBackpressureDrop(); + } + /** + * @return a singleton instance of this stateless operator. + */ + @SuppressWarnings({ "unchecked" }) + public static OperatorOnBackpressureDrop instance() { + return (OperatorOnBackpressureDrop)Holder.INSTANCE; + } + private OperatorOnBackpressureDrop() { } @Override public Subscriber call(final Subscriber child) { final AtomicLong requested = new AtomicLong(); diff --git a/src/main/java/rx/internal/operators/OperatorSerialize.java b/src/main/java/rx/internal/operators/OperatorSerialize.java index 0790c4864c..334ddef679 100644 --- a/src/main/java/rx/internal/operators/OperatorSerialize.java +++ b/src/main/java/rx/internal/operators/OperatorSerialize.java @@ -20,7 +20,19 @@ import rx.observers.SerializedSubscriber; public final class OperatorSerialize implements Operator { - + /** Lazy initialization via inner-class holder. */ + private static final class Holder { + /** A singleton instance. */ + static final OperatorSerialize INSTANCE = new OperatorSerialize(); + } + /** + * @return a singleton instance of this stateless operator. + */ + @SuppressWarnings({ "unchecked" }) + public static OperatorSerialize instance() { + return (OperatorSerialize)Holder.INSTANCE; + } + private OperatorSerialize() { } @Override public Subscriber call(final Subscriber s) { return new SerializedSubscriber(new Subscriber(s) { diff --git a/src/main/java/rx/internal/operators/OperatorSwitch.java b/src/main/java/rx/internal/operators/OperatorSwitch.java index e0e1a9213a..7ee71084aa 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitch.java +++ b/src/main/java/rx/internal/operators/OperatorSwitch.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; + import rx.Observable; import rx.Observable.Operator; import rx.Producer; @@ -33,7 +34,19 @@ * @param the value type */ public final class OperatorSwitch implements Operator> { - + /** Lazy initialization via inner-class holder. */ + private static final class Holder { + /** A singleton instance. */ + static final OperatorSwitch INSTANCE = new OperatorSwitch(); + } + /** + * @return a singleton instance of this stateless operator. + */ + @SuppressWarnings({ "unchecked" }) + public static OperatorSwitch instance() { + return (OperatorSwitch)Holder.INSTANCE; + } + private OperatorSwitch() { } @Override public Subscriber> call(final Subscriber child) { return new SwitchSubscriber(child); diff --git a/src/main/java/rx/internal/operators/OperatorToObservableList.java b/src/main/java/rx/internal/operators/OperatorToObservableList.java index 97439f53b7..fef13577cc 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableList.java @@ -37,7 +37,19 @@ * as you do not have the option to unsubscribe. */ public final class OperatorToObservableList implements Operator, T> { - + /** Lazy initialization via inner-class holder. */ + private static final class Holder { + /** A singleton instance. */ + static final OperatorToObservableList INSTANCE = new OperatorToObservableList(); + } + /** + * @return a singleton instance of this stateless operator. + */ + @SuppressWarnings({ "unchecked" }) + public static OperatorToObservableList instance() { + return (OperatorToObservableList)Holder.INSTANCE; + } + private OperatorToObservableList() { } @Override public Subscriber call(final Subscriber> o) { return new Subscriber(o) { diff --git a/src/test/java/rx/internal/operators/OperatorToObservableListTest.java b/src/test/java/rx/internal/operators/OperatorToObservableListTest.java index c1a9456ae3..298c6f5f62 100644 --- a/src/test/java/rx/internal/operators/OperatorToObservableListTest.java +++ b/src/test/java/rx/internal/operators/OperatorToObservableListTest.java @@ -29,14 +29,13 @@ import rx.Observable; import rx.Observer; -import rx.internal.operators.OperatorToObservableList; public class OperatorToObservableListTest { @Test public void testList() { Observable w = Observable.from(Arrays.asList("one", "two", "three")); - Observable> observable = w.lift(new OperatorToObservableList()); + Observable> observable = w.toList(); @SuppressWarnings("unchecked") Observer> observer = mock(Observer.class); @@ -62,7 +61,7 @@ public void testListViaObservable() { @Test public void testListMultipleObservers() { Observable w = Observable.from(Arrays.asList("one", "two", "three")); - Observable> observable = w.lift(new OperatorToObservableList()); + Observable> observable = w.toList(); @SuppressWarnings("unchecked") Observer> o1 = mock(Observer.class); @@ -86,7 +85,7 @@ public void testListMultipleObservers() { @Test public void testListWithNullValue() { Observable w = Observable.from(Arrays.asList("one", null, "three")); - Observable> observable = w.lift(new OperatorToObservableList()); + Observable> observable = w.toList(); @SuppressWarnings("unchecked") Observer> observer = mock(Observer.class); From b37c7ed133f5b10161d130f8cccc510a249e4d1d Mon Sep 17 00:00:00 2001 From: David Gross Date: Mon, 12 Jan 2015 13:14:06 -0800 Subject: [PATCH 051/857] updating javadocs to link to latest web docs --- src/main/java/rx/Observable.java | 637 +++++++++--------- src/main/java/rx/Observer.java | 2 +- src/main/java/rx/Subscriber.java | 2 +- .../rx/observables/BlockingObservable.java | 38 +- .../rx/observables/ConnectableObservable.java | 3 + .../rx/observables/GroupedObservable.java | 2 +- 6 files changed, 340 insertions(+), 344 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 6162b1339c..21b503a9b5 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -39,7 +39,8 @@ *

* *

- * For more information see the RxJava wiki + * For more information see the ReactiveX + * documentation. * * @param * the type of the items emitted by the Observable @@ -90,7 +91,7 @@ protected Observable(OnSubscribe f) { * {@code onError}, and {@code onCompleted} methods as appropriate * @return an Observable that, when a {@link Subscriber} subscribes to it, will execute the specified * function - * @see RxJava wiki: create + * @see ReactiveX operators documentation: Create */ public final static Observable create(OnSubscribe f) { return new Observable(hook.onCreate(f)); @@ -216,7 +217,7 @@ public static interface Transformer extends Func1, Observabl * an Iterable of Observable sources competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Iterable> sources) { return create(OnSubscribeAmb.amb(sources)); @@ -237,7 +238,7 @@ public final static Observable amb(IterableRxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2) { return create(OnSubscribeAmb.amb(o1, o2)); @@ -260,7 +261,7 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3) { return create(OnSubscribeAmb.amb(o1, o2, o3)); @@ -285,7 +286,7 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4)); @@ -312,7 +313,7 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5)); @@ -341,7 +342,7 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6)); @@ -372,7 +373,7 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6, o7)); @@ -405,7 +406,7 @@ public final static Observable amb(Observable o1, Observable * an observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8)); @@ -440,7 +441,7 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8, o9)); @@ -465,7 +466,7 @@ public final static Observable amb(Observable o1, Observable * 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 RxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Func2 combineFunction) { @@ -493,7 +494,7 @@ public static final Observable combineLatest(ObservableRxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Func3 combineFunction) { @@ -523,7 +524,7 @@ public static final Observable combineLatest(ObservableRxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, @@ -556,7 +557,7 @@ public static final Observable combineLatest(ObservableRxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, @@ -591,7 +592,7 @@ public static final Observable combineLatest(Observab * 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 RxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, @@ -628,7 +629,7 @@ public static final Observable combineLatest(Obse * 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 RxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, @@ -667,7 +668,7 @@ public static final Observable combineLatest( * 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 RxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, @@ -708,7 +709,7 @@ public static final Observable combineLat * 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 RxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, @@ -735,7 +736,7 @@ public static final Observable combin * 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 RxJava wiki: combineLatest + * @see ReactiveX operators documentation: CombineLatest */ public static final Observable combineLatest(List> sources, FuncN combineFunction) { return create(new OnSubscribeCombineLatest(sources, combineFunction)); @@ -755,7 +756,7 @@ public static final Observable combineLatest(ListRxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable> observables) { return observables.lift(new OperatorConcat()); @@ -777,7 +778,7 @@ public final static Observable concat(ObservableRxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2) { return concat(just(t1, t2)); @@ -801,7 +802,7 @@ public final static Observable concat(Observable t1, Observa * an Observable to be concatenated * @return an Observable that emits items emitted by the three source Observables, one after the other, * without interleaving them - * @see RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2, Observable t3) { return concat(just(t1, t2, t3)); @@ -827,7 +828,7 @@ public final static Observable concat(Observable t1, Observa * an Observable to be concatenated * @return an Observable that emits items emitted by the four source Observables, one after the other, * without interleaving them - * @see RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4) { return concat(just(t1, t2, t3, t4)); @@ -855,7 +856,7 @@ public final static Observable concat(Observable t1, Observa * an Observable to be concatenated * @return an Observable that emits items emitted by the five source Observables, one after the other, * without interleaving them - * @see RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { return concat(just(t1, t2, t3, t4, t5)); @@ -885,7 +886,7 @@ public final static Observable concat(Observable t1, Observa * an Observable to be concatenated * @return an Observable that emits items emitted by the six source Observables, one after the other, * without interleaving them - * @see RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { return concat(just(t1, t2, t3, t4, t5, t6)); @@ -917,7 +918,7 @@ public final static Observable concat(Observable t1, Observa * an Observable to be concatenated * @return an Observable that emits items emitted by the seven source Observables, one after the other, * without interleaving them - * @see RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { return concat(just(t1, t2, t3, t4, t5, t6, t7)); @@ -951,7 +952,7 @@ public final static Observable concat(Observable t1, Observa * an Observable to be concatenated * @return an Observable that emits items emitted by the eight source Observables, one after the other, * without interleaving them - * @see RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { return concat(just(t1, t2, t3, t4, t5, t6, t7, t8)); @@ -987,7 +988,7 @@ public final static Observable concat(Observable t1, Observa * an Observable to be concatenated * @return an Observable that emits items emitted by the nine source Observables, one after the other, * without interleaving them - * @see RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { return concat(just(t1, t2, t3, t4, t5, t6, t7, t8, t9)); @@ -1015,7 +1016,7 @@ public final static Observable concat(Observable t1, Observa * the type of the items emitted by the Observable * @return an Observable whose {@link Observer}s' subscriptions trigger an invocation of the given * Observable factory function - * @see RxJava wiki: defer + * @see ReactiveX operators documentation: Defer */ public final static Observable defer(Func0> observableFactory) { return create(new OnSubscribeDefer(observableFactory)); @@ -1035,7 +1036,7 @@ public final static Observable defer(Func0> observableFacto * the type of the items (ostensibly) emitted by the Observable * @return an Observable that emits no items to the {@link Observer} but immediately invokes the * {@link Observer}'s {@link Observer#onCompleted() onCompleted} method - * @see RxJava wiki: empty + * @see ReactiveX operators documentation: Empty */ public final static Observable empty() { return from(Collections.emptyList()); @@ -1057,7 +1058,7 @@ public final static Observable empty() { * the type of the items (ostensibly) emitted by the Observable * @return an Observable that invokes the {@link Observer}'s {@link Observer#onError onError} method when * the Observer subscribes to it - * @see RxJava wiki: error + * @see ReactiveX operators documentation: Throw */ public final static Observable error(Throwable exception) { return new ThrowObservable(exception); @@ -1084,7 +1085,7 @@ public final static Observable error(Throwable exception) { * the type of object that the {@link Future} returns, and also the type of item to be emitted by * the resulting Observable * @return an Observable that emits the item from the source {@link Future} - * @see RxJava wiki: from + * @see ReactiveX operators documentation: From */ public final static Observable from(Future future) { return create(OnSubscribeToObservableFuture.toObservableFuture(future)); @@ -1115,7 +1116,7 @@ public final static Observable from(Future future) { * the type of object that the {@link Future} returns, and also the type of item to be emitted by * the resulting Observable * @return an Observable that emits the item from the source {@link Future} - * @see RxJava wiki: from + * @see ReactiveX operators documentation: From */ public final static Observable from(Future future, long timeout, TimeUnit unit) { return create(OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); @@ -1143,7 +1144,7 @@ public final static Observable from(Future future, long time * the type of object that the {@link Future} returns, and also the type of item to be emitted by * the resulting Observable * @return an Observable that emits the item from the source {@link Future} - * @see RxJava wiki: from + * @see ReactiveX operators documentation: From */ public final 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 @@ -1165,7 +1166,7 @@ public final static Observable from(Future future, Scheduler * the type of items in the {@link Iterable} sequence and the type of items to be emitted by the * resulting Observable * @return an Observable that emits each item in the source {@link Iterable} sequence - * @see RxJava wiki: from + * @see ReactiveX operators documentation: From */ public final static Observable from(Iterable iterable) { return create(new OnSubscribeFromIterable(iterable)); @@ -1185,7 +1186,7 @@ public final static Observable from(Iterable iterable) { * @param * the type of items in the Array and the type of items to be emitted by the resulting Observable * @return an Observable that emits each item in the source Array - * @see RxJava wiki: from + * @see ReactiveX operators documentation: From */ public final static Observable from(T[] array) { return from(Arrays.asList(array)); @@ -1205,7 +1206,7 @@ public final static Observable from(T[] array) { * @param unit * time units to use for the interval size * @return an Observable that emits a sequential number each time interval - * @see RxJava wiki: interval + * @see ReactiveX operators documentation: Interval */ public final static Observable interval(long interval, TimeUnit unit) { return interval(interval, unit, Schedulers.computation()); @@ -1228,7 +1229,7 @@ public final static Observable interval(long interval, TimeUnit unit) { * @param scheduler * the Scheduler to use for scheduling the items * @return an Observable that emits a sequential number each time interval - * @see RxJava wiki: interval + * @see ReactiveX operators documentation: Interval */ public final static Observable interval(long interval, TimeUnit unit, Scheduler scheduler) { return create(new OnSubscribeTimerPeriodically(interval, interval, unit, scheduler)); @@ -1256,7 +1257,7 @@ public final static Observable interval(long interval, TimeUnit unit, Sche * @param * the type of that item * @return an Observable that emits {@code value} as a single item and then completes - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ public final static Observable just(final T value) { return ScalarSynchronousObservable.create(value); @@ -1278,7 +1279,7 @@ public final static Observable just(final T value) { * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1304,7 +1305,7 @@ public final static Observable just(T t1, T t2) { * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1332,7 +1333,7 @@ public final static Observable just(T t1, T t2, T t3) { * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1362,7 +1363,7 @@ public final static Observable just(T t1, T t2, T t3, T t4) { * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1394,7 +1395,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5) { * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1428,7 +1429,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6) { * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1464,7 +1465,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1502,7 +1503,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1542,7 +1543,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T * @param * the type of these items * @return an Observable that emits each item - * @see RxJava wiki: just + * @see ReactiveX operators documentation: Just */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") @@ -1566,7 +1567,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T * the Iterable of Observables * @return an Observable that emits items that are the result of flattening the items emitted by the * Observables in the Iterable - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ public final static Observable merge(Iterable> sequences) { return merge(from(sequences)); @@ -1593,7 +1594,7 @@ public final static Observable merge(IterableRxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ public final static Observable merge(Iterable> sequences, int maxConcurrent) { return merge(from(sequences), maxConcurrent); @@ -1616,7 +1617,7 @@ public final static Observable merge(IterableRxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ public final static Observable merge(Observable> source) { return source.lift(new OperatorMerge()); @@ -1644,7 +1645,7 @@ public final static Observable merge(ObservableRxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ public final static Observable merge(Observable> source, int maxConcurrent) { return source.lift(new OperatorMergeMaxConcurrent(maxConcurrent)); @@ -1667,7 +1668,7 @@ public final static Observable merge(ObservableRxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2) { @@ -1693,7 +1694,7 @@ public final static Observable merge(Observable t1, Observab * @param t3 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3) { @@ -1721,7 +1722,7 @@ public final static Observable merge(Observable t1, Observab * @param t4 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4) { @@ -1751,7 +1752,7 @@ public final static Observable merge(Observable t1, Observab * @param t5 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { @@ -1783,7 +1784,7 @@ public final static Observable merge(Observable t1, Observab * @param t6 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { @@ -1817,7 +1818,7 @@ public final static Observable merge(Observable t1, Observab * @param t7 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { @@ -1853,7 +1854,7 @@ public final static Observable merge(Observable t1, Observab * @param t8 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { @@ -1891,7 +1892,7 @@ public final static Observable merge(Observable t1, Observab * @param t9 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { @@ -1913,7 +1914,7 @@ public final static Observable merge(Observable t1, Observab * @param sequences * the Array of Observables * @return an Observable that emits all of the items emitted by the Observables in the Array - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ public final static Observable merge(Observable[] sequences) { return merge(from(sequences)); @@ -1941,7 +1942,7 @@ public final static Observable merge(Observable[] sequences) * an Observable that emits Observables * @return an Observable that emits all of the items emitted by the Observables emitted by the * {@code source} Observable - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable> source) { return source.lift(new OperatorMergeDelayError()); @@ -1970,7 +1971,7 @@ public final static Observable mergeDelayError(ObservableRxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable t1, Observable t2) { return mergeDelayError(just(t1, t2)); @@ -2002,7 +2003,7 @@ public final static Observable mergeDelayError(Observable t1 * @param t3 * an Observable to be merged * @return an Observable that emits all of the items that are emitted by the source Observables - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3) { return mergeDelayError(just(t1, t2, t3)); @@ -2036,7 +2037,7 @@ public final static Observable mergeDelayError(Observable t1 * @param t4 * an Observable to be merged * @return an Observable that emits all of the items that are emitted by the source Observables - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4) { return mergeDelayError(just(t1, t2, t3, t4)); @@ -2072,7 +2073,7 @@ public final static Observable mergeDelayError(Observable t1 * @param t5 * an Observable to be merged * @return an Observable that emits all of the items that are emitted by the source Observables - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { return mergeDelayError(just(t1, t2, t3, t4, t5)); @@ -2110,7 +2111,7 @@ public final static Observable mergeDelayError(Observable t1 * @param t6 * an Observable to be merged * @return an Observable that emits all of the items that are emitted by the source Observables - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { return mergeDelayError(just(t1, t2, t3, t4, t5, t6)); @@ -2151,7 +2152,7 @@ public final static Observable mergeDelayError(Observable t1 * @param t7 * an Observable to be merged * @return an Observable that emits all of the items that are emitted by the source Observables - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { return mergeDelayError(just(t1, t2, t3, t4, t5, t6, t7)); @@ -2193,7 +2194,7 @@ public final static Observable mergeDelayError(Observable t1 * @param t8 * an Observable to be merged * @return an Observable that emits all of the items that are emitted by the source Observables - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ // suppress because the types are checked by the method signature before using a vararg public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { @@ -2238,7 +2239,7 @@ public final static Observable mergeDelayError(Observable t1 * @param t9 * an Observable to be merged * @return an Observable that emits all of the items that are emitted by the source Observables - * @see RxJava wiki: mergeDelayError + * @see ReactiveX operators documentation: Merge */ public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { return mergeDelayError(just(t1, t2, t3, t4, t5, t6, t7, t8, t9)); @@ -2255,6 +2256,7 @@ public final static Observable mergeDelayError(Observable t1 * * * @return an Observable that emits a single item: the source Observable + * @see ReactiveX operators documentation: To */ public final Observable> nest() { return just(this); @@ -2274,7 +2276,7 @@ public final Observable> nest() { * @param * the type of items (not) emitted by the Observable * @return an Observable that never emits any items or sends any notifications to an {@link Observer} - * @see RxJava wiki: never + * @see ReactiveX operators documentation: Never */ public final static Observable never() { return new NeverObservable(); @@ -2297,7 +2299,7 @@ public final static Observable never() { * @throws IllegalArgumentException * if {@code count} is less than zero, or if {@code start} + {@code count} − 1 exceeds * {@code Integer.MAX_VALUE} - * @see RxJava wiki: range + * @see ReactiveX operators documentation: Range */ public final static Observable range(int start, int count) { if (count < 0) { @@ -2332,7 +2334,7 @@ public final static Observable range(int start, int count) { * @param scheduler * the Scheduler to run the generator loop on * @return an Observable that emits a range of sequential Integers - * @see RxJava wiki: range + * @see ReactiveX operators documentation: Range */ public final static Observable range(int start, int count, Scheduler scheduler) { return range(start, count).subscribeOn(scheduler); @@ -2355,7 +2357,7 @@ public final static Observable range(int start, int count, Scheduler sc * @param * the type of items emitted by each Observable * @return an Observable that emits a Boolean value that indicates whether the two sequences are the same - * @see RxJava wiki: sequenceEqual + * @see ReactiveX operators documentation: SequenceEqual */ public final static Observable sequenceEqual(Observable first, Observable second) { return sequenceEqual(first, second, new Func2() { @@ -2390,7 +2392,7 @@ public final Boolean call(T first, T second) { * the type of items emitted by each Observable * @return an Observable that emits a Boolean value that indicates whether the two Observable two sequences * are the same according to the specified function - * @see RxJava wiki: sequenceEqual + * @see ReactiveX operators documentation: SequenceEqual */ public final static Observable sequenceEqual(Observable first, Observable second, Func2 equality) { return OperatorSequenceEqual.sequenceEqual(first, second, equality); @@ -2416,7 +2418,7 @@ public final static Observable sequenceEqual(ObservableRxJava wiki: switchOnNext + * @see ReactiveX operators documentation: Switch */ public final static Observable switchOnNext(Observable> sequenceOfSequences) { return sequenceOfSequences.lift(new OperatorSwitch()); @@ -2443,7 +2445,7 @@ public final static Observable switchOnNext(ObservableRxJava wiki: timer + * @see ReactiveX operators documentation: Timer */ public final static Observable timer(long initialDelay, long period, TimeUnit unit) { return timer(initialDelay, period, unit, Schedulers.computation()); @@ -2472,7 +2474,7 @@ public final static Observable timer(long initialDelay, long period, TimeU * the Scheduler on which the waiting happens and items are emitted * @return an Observable that emits a 0L after the {@code initialDelay} and ever increasing numbers after * each {@code period} of time thereafter, while running on the given Scheduler - * @see RxJava wiki: timer + * @see ReactiveX operators documentation: Timer */ public final static Observable timer(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { return create(new OnSubscribeTimerPeriodically(initialDelay, period, unit, scheduler)); @@ -2495,7 +2497,7 @@ public final static Observable timer(long initialDelay, long period, TimeU * @param unit * time units to use for {@code delay} * @return an Observable that emits one item after a specified delay, and then completes - * @see RxJava wiki: timer + * @see ReactiveX operators documentation: Timer */ public final static Observable timer(long delay, TimeUnit unit) { return timer(delay, unit, Schedulers.computation()); @@ -2522,7 +2524,7 @@ public final static Observable timer(long delay, TimeUnit unit) { * the {@link Scheduler} to use for scheduling the item * @return an Observable that emits one item after a specified delay, on a specified Scheduler, and then * completes - * @see RxJava wiki: timer + * @see ReactiveX operators documentation: Timer */ public final static Observable timer(long delay, TimeUnit unit, Scheduler scheduler) { return create(new OnSubscribeTimerOnce(delay, unit, scheduler)); @@ -2544,7 +2546,7 @@ public final static Observable timer(long delay, TimeUnit unit, Scheduler * @param disposeAction * the function that will dispose of the resource * @return the Observable whose lifetime controls the lifetime of the dependent resource object - * @see RxJava wiki: using + * @see ReactiveX operators documentation: Using */ public final static Observable using( final Func0 resourceFactory, @@ -2577,7 +2579,7 @@ public final static Observable using( * a function that, when applied to an item emitted by each of the source Observables, results in * an item that will be emitted by the resulting Observable * @return an Observable that emits the zipped results - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Iterable> ws, FuncN zipFunction) { List> os = new ArrayList>(); @@ -2611,7 +2613,7 @@ public final static Observable zip(Iterable> ws, * a function that, when applied to an item emitted by each of the Observables emitted by * {@code ws}, results in an item that will be emitted by the resulting Observable * @return an Observable that emits the zipped results - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable> ws, final FuncN zipFunction) { return ws.toList().map(new Func1>, Observable[]>() { @@ -2651,7 +2653,7 @@ public Observable[] call(List> o) { * a function that, when applied to an item emitted by each of the source Observables, results * in an item that will be emitted by the resulting Observable * @return an Observable that emits the zipped results - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, final Func2 zipFunction) { return just(new Observable[] { o1, o2 }).lift(new OperatorZip(zipFunction)); @@ -2687,7 +2689,7 @@ public final static Observable zip(Observable o1, O * a function that, when applied to an item emitted by each of the source Observables, results in * an item that will be emitted by the resulting Observable * @return an Observable that emits the zipped results - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, Observable o3, Func3 zipFunction) { return just(new Observable[] { o1, o2, o3 }).lift(new OperatorZip(zipFunction)); @@ -2725,7 +2727,7 @@ public final static Observable zip(Observable o * a function that, when applied to an item emitted by each of the source Observables, results in * an item that will be emitted by the resulting Observable * @return an Observable that emits the zipped results - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Func4 zipFunction) { return just(new Observable[] { o1, o2, o3, o4 }).lift(new OperatorZip(zipFunction)); @@ -2765,7 +2767,7 @@ public final static Observable zip(ObservableRxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Func5 zipFunction) { return just(new Observable[] { o1, o2, o3, o4, o5 }).lift(new OperatorZip(zipFunction)); @@ -2806,7 +2808,7 @@ public final static Observable zip(ObservableRxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Func6 zipFunction) { @@ -2850,7 +2852,7 @@ public final static Observable zip(ObservableRxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Func7 zipFunction) { @@ -2896,7 +2898,7 @@ public final static Observable zip(Observable * a function that, when applied to an item emitted by each of the source Observables, results in * an item that will be emitted by the resulting Observable * @return an Observable that emits the zipped results - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Func8 zipFunction) { @@ -2944,7 +2946,7 @@ public final static Observable zip(Observ * a function that, when applied to an item emitted by each of the source Observables, results in * an item that will be emitted by the resulting Observable * @return an Observable that emits the zipped results - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9, Func9 zipFunction) { @@ -2965,7 +2967,7 @@ public final static Observable zip(Ob * a function that evaluates an item and returns a Boolean * @return an Observable that emits {@code true} if all items emitted by the source Observable satisfy the * predicate; otherwise, {@code false} - * @see RxJava wiki: all + * @see ReactiveX operators documentation: All */ public final Observable all(Func1 predicate) { return lift(new OperatorAll(predicate)); @@ -2984,7 +2986,7 @@ public final Observable all(Func1 predicate) { * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of the source Observables first * emitted an item - * @see RxJava wiki: amb + * @see ReactiveX operators documentation: Amb */ public final Observable ambWith(Observable t1) { return amb(this, t1); @@ -3025,7 +3027,7 @@ public final Observable asObservable() { * begins to fill a new one * @return an Observable that emits a connected, non-overlapping buffer of items from the source Observable * each time the Observable created with the {@code bufferClosingSelector} argument emits an item - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(Func0> bufferClosingSelector) { return lift(new OperatorBufferWithSingleObservable(bufferClosingSelector, 16)); @@ -3047,7 +3049,7 @@ public final Observable> buffer(Func0RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(int count) { return buffer(count, count); @@ -3073,7 +3075,7 @@ public final Observable> buffer(int count) { * {@link #buffer(int)}. * @return an Observable that emits buffers for every {@code skip} item from the source Observable and * containing at most {@code count} items - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(int count, int skip) { return lift(new OperatorBufferWithSize(count, skip)); @@ -3103,7 +3105,7 @@ public final Observable> buffer(int count, int skip) { * the unit of time that applies to the {@code timespan} and {@code timeshift} arguments * @return an Observable that emits new buffers of items emitted by the source Observable periodically after * a fixed timespan has elapsed - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(long timespan, long timeshift, TimeUnit unit) { return buffer(timespan, timeshift, unit, Schedulers.computation()); @@ -3135,7 +3137,7 @@ public final Observable> buffer(long timespan, long timeshift, TimeUnit * the {@link Scheduler} to use when determining the end and start of a buffer * @return an Observable that emits new buffers of items emitted by the source Observable periodically after * a fixed timespan has elapsed - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(long timespan, long timeshift, TimeUnit unit, Scheduler scheduler) { return lift(new OperatorBufferWithTime(timespan, timeshift, unit, Integer.MAX_VALUE, scheduler)); @@ -3163,7 +3165,7 @@ public final Observable> buffer(long timespan, long timeshift, TimeUnit * the unit of time that applies to the {@code timespan} argument * @return an Observable that emits connected, non-overlapping buffers of items emitted by the source * Observable within a fixed duration - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(long timespan, TimeUnit unit) { return buffer(timespan, unit, Integer.MAX_VALUE, Schedulers.computation()); @@ -3195,7 +3197,7 @@ public final Observable> buffer(long timespan, TimeUnit unit) { * @return an Observable that emits connected, non-overlapping buffers of items emitted by the source * Observable, after a fixed duration or when the buffer reaches maximum capacity (whichever occurs * first) - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(long timespan, TimeUnit unit, int count) { return lift(new OperatorBufferWithTime(timespan, timespan, unit, count, Schedulers.computation())); @@ -3230,7 +3232,7 @@ public final Observable> buffer(long timespan, TimeUnit unit, int count) * @return an Observable that emits connected, non-overlapping buffers of items emitted by the source * Observable after a fixed duration or when the buffer reaches maximum capacity (whichever occurs * first) - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(long timespan, TimeUnit unit, int count, Scheduler scheduler) { return lift(new OperatorBufferWithTime(timespan, timespan, unit, count, scheduler)); @@ -3261,7 +3263,7 @@ public final Observable> buffer(long timespan, TimeUnit unit, int count, * the {@link Scheduler} to use when determining the end and start of a buffer * @return an Observable that emits connected, non-overlapping buffers of items emitted by the source * Observable within a fixed duration - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(long timespan, TimeUnit unit, Scheduler scheduler) { return buffer(timespan, timespan, unit, scheduler); @@ -3288,7 +3290,7 @@ public final Observable> buffer(long timespan, TimeUnit unit, Scheduler * Observable emits an item, the associated buffer is emitted. * @return an Observable that emits buffers, containing items from the source Observable, that are created * and closed when the specified Observables emit items - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(Observable bufferOpenings, Func1> bufferClosingSelector) { return lift(new OperatorBufferWithStartEndObservable(bufferOpenings, bufferClosingSelector)); @@ -3318,7 +3320,7 @@ public final Observable> buffer(ObservableRxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer */ public final Observable> buffer(Observable boundary) { return buffer(boundary, 16); @@ -3349,7 +3351,7 @@ public final Observable> buffer(Observable boundary) { * the initial capacity of each buffer chunk * @return an Observable that emits buffered items from the source Observable when the boundary Observable * emits an item - * @see RxJava wiki: buffer + * @see ReactiveX operators documentation: Buffer * @see #buffer(rx.Observable, int) */ public final Observable> buffer(Observable boundary, int initialCapacity) { @@ -3384,7 +3386,7 @@ public final Observable> buffer(Observable boundary, int initialC * * @return an Observable that, when first subscribed to, caches all of its items and notifications for the * benefit of subsequent subscribers - * @see RxJava wiki: cache + * @see ReactiveX operators documentation: Replay */ public final Observable cache() { return create(new OnSubscribeCache(this)); @@ -3419,7 +3421,7 @@ public final Observable cache() { * @param capacity hint for number of items to cache (for optimizing underlying data structure) * @return an Observable that, when first subscribed to, caches all of its items and notifications for the * benefit of subsequent subscribers - * @see RxJava wiki: cache + * @see ReactiveX operators documentation: Replay */ public final Observable cache(int capacity) { return create(new OnSubscribeCache(this, capacity)); @@ -3440,7 +3442,7 @@ public final Observable cache(int capacity) { * into before emitting them from the resulting Observable * @return an Observable that emits each item from the source Observable after converting it to the * specified type - * @see RxJava wiki: cast + * @see ReactiveX operators documentation: Map */ public final Observable cast(final Class klass) { return lift(new OperatorCast(klass)); @@ -3468,7 +3470,7 @@ public final Observable cast(final Class klass) { * accordingly * @return an Observable that emits the result of collecting the values emitted by the source Observable * into a single mutable data structure - * @see RxJava wiki: collect + * @see ReactiveX operators documentation: Reduce */ public final Observable collect(Func0 stateFactory, final Action2 collector) { Func2 accumulator = new Func2() { @@ -3506,7 +3508,7 @@ public final R call(R state, T value) { * Observable * @return an Observable that emits the result of applying the transformation function to each item emitted * by the source Observable and concatinating the Observables obtained from this transformation - * @see RxJava wiki: concatMap + * @see ReactiveX operators documentation: FlatMap */ public final Observable concatMap(Func1> func) { return concat(map(func)); @@ -3526,7 +3528,7 @@ public final Observable concatMap(Func1RxJava wiki: concat + * @see ReactiveX operators documentation: Concat */ public final Observable concatWith(Observable t1) { return concat(this, t1); @@ -3546,7 +3548,7 @@ public final Observable concatWith(Observable t1) { * the item to search for in the emissions from the source Observable * @return an Observable that emits {@code true} if the specified item is emitted by the source Observable, * or {@code false} if the source Observable completes without emitting that item - * @see RxJava wiki: contains + * @see ReactiveX operators documentation: Contains */ public final Observable contains(final Object element) { return exists(new Func1() { @@ -3570,7 +3572,7 @@ public final Boolean call(T t1) { * * * @return an Observable that emits a single item: the number of elements emitted by the source Observable - * @see RxJava wiki: count + * @see ReactiveX operators documentation: Count * @see #countLong() */ public final Observable count() { @@ -3597,7 +3599,7 @@ public final Integer call(Integer t1, T t2) { * * @return an Observable that emits a single item: the number of items emitted by the source Observable as a * 64-bit Long item - * @see RxJava wiki: countLong + * @see ReactiveX operators documentation: Count * @see #count() */ public final Observable countLong() { @@ -3628,7 +3630,7 @@ public final Long call(Long t1, T t2) { * function to retrieve a sequence that indicates the throttle duration for each item * @return an Observable that omits items emitted by the source Observable that are followed by another item * within a computed debounce duration - * @see RxJava wiki: debounce + * @see ReactiveX operators documentation: Debounce * @see RxJava wiki: Backpressure */ public final Observable debounce(Func1> debounceSelector) { @@ -3666,7 +3668,7 @@ public final Observable debounce(Func1 * the {@link TimeUnit} for the timeout * @return an Observable that filters out items from the source Observable that are too quickly followed by * newer items - * @see RxJava wiki: debounce + * @see ReactiveX operators documentation: Debounce * @see RxJava wiki: Backpressure * @see #throttleWithTimeout(long, TimeUnit) */ @@ -3708,7 +3710,7 @@ public final Observable debounce(long timeout, TimeUnit unit) { * item * @return an Observable that filters out items from the source Observable that are too quickly followed by * newer items - * @see RxJava wiki: debounce + * @see ReactiveX operators documentation: Debounce * @see RxJava wiki: Backpressure * @see #throttleWithTimeout(long, TimeUnit, Scheduler) */ @@ -3730,7 +3732,7 @@ public final Observable debounce(long timeout, TimeUnit unit, Scheduler sched * the item to emit if the source Observable emits no items * @return an Observable that emits either the specified default item if the source Observable emits no * items, or the items emitted by the source Observable - * @see RxJava wiki: defaultIfEmpty + * @see ReactiveX operators documentation: DefaultIfEmpty */ public final Observable defaultIfEmpty(T defaultValue) { return lift(new OperatorDefaultIfEmpty(defaultValue)); @@ -3762,7 +3764,7 @@ public final Observable defaultIfEmpty(T defaultValue) { * returned from {@code itemDelay} emits an item * @return an Observable that delays the subscription and emissions of the source Observable via another * Observable on a per-item basis - * @see RxJava wiki: delay + * @see ReactiveX operators documentation: Delay */ public final Observable delay( Func0> subscriptionDelay, @@ -3791,7 +3793,7 @@ public final Observable delay( * returned from {@code itemDelay} emits an item * @return an Observable that delays the emissions of the source Observable via another Observable on a * per-item basis - * @see RxJava wiki: delay + * @see ReactiveX operators documentation: Delay */ public final Observable delay(Func1> itemDelay) { return lift(new OperatorDelayWithSelector(this, itemDelay)); @@ -3812,7 +3814,7 @@ public final Observable delay(Func1> i * @param unit * the {@link TimeUnit} in which {@code period} is defined * @return the source Observable shifted in time by the specified delay - * @see RxJava wiki: delay + * @see ReactiveX operators documentation: Delay */ public final Observable delay(long delay, TimeUnit unit) { return delay(delay, unit, Schedulers.computation()); @@ -3835,7 +3837,7 @@ public final Observable delay(long delay, TimeUnit unit) { * @param scheduler * the {@link Scheduler} to use for delaying * @return the source Observable shifted in time by the specified delay - * @see RxJava wiki: delay + * @see ReactiveX operators documentation: Delay */ public final Observable delay(long delay, TimeUnit unit, Scheduler scheduler) { return lift(new OperatorDelay(this, delay, unit, scheduler)); @@ -3855,7 +3857,7 @@ public final Observable delay(long delay, TimeUnit unit, Scheduler scheduler) * @param unit * the time unit of {@code delay} * @return an Observable that delays the subscription to the source Observable by the given amount - * @see RxJava wiki: delaySubscription + * @see ReactiveX operators documentation: Delay */ public final Observable delaySubscription(long delay, TimeUnit unit) { return delaySubscription(delay, unit, Schedulers.computation()); @@ -3879,7 +3881,7 @@ public final Observable delaySubscription(long delay, TimeUnit unit) { * the Scheduler on which the waiting and subscription will happen * @return an Observable that delays the subscription to the source Observable by a given * amount, waiting and subscribing on the given Scheduler - * @see RxJava wiki: delaySubscription + * @see ReactiveX operators documentation: Delay */ public final Observable delaySubscription(long delay, TimeUnit unit, Scheduler scheduler) { return create(new OnSubscribeDelaySubscription(this, delay, unit, scheduler)); @@ -3900,7 +3902,7 @@ public final Observable delaySubscription(long delay, TimeUnit unit, Schedule * once it emits any item * @return an Observable that delays the subscription to the source Observable until the Observable returned * by {@code subscriptionDelay} emits an item - * @see RxJava wiki: delaySubscription + * @see ReactiveX operators documentation: Delay */ public final Observable delaySubscription(Func0> subscriptionDelay) { return create(new OnSubscribeDelaySubscriptionWithSelector(this, subscriptionDelay)); @@ -3921,7 +3923,7 @@ public final Observable delaySubscription(Func0> * emitted by the source Observable * @throws OnErrorNotImplementedException * if the source Observable is not of type {@code Observable>} - * @see RxJava wiki: dematerialize + * @see ReactiveX operators documentation: Dematerialize */ @SuppressWarnings({"unchecked", "rawtypes"}) public final Observable dematerialize() { @@ -3939,7 +3941,7 @@ public final Observable dematerialize() { * * @return an Observable that emits only those items emitted by the source Observable that are distinct from * each other - * @see RxJava wiki: distinct + * @see ReactiveX operators documentation: Distinct */ public final Observable distinct() { return lift(new OperatorDistinct(UtilityFunctions.identity())); @@ -3959,7 +3961,7 @@ public final Observable distinct() { * a function that projects an emitted item to a key value that is used to decide whether an item * is distinct from another one or not * @return an Observable that emits those items emitted by the source Observable that have distinct keys - * @see RxJava wiki: distinct + * @see ReactiveX operators documentation: Distinct */ public final Observable distinct(Func1 keySelector) { return lift(new OperatorDistinct(keySelector)); @@ -3977,7 +3979,7 @@ public final Observable distinct(Func1 keySelecto * * @return an Observable that emits those items from the source Observable that are distinct from their * immediate predecessors - * @see RxJava wiki: distinctUntilChanged + * @see ReactiveX operators documentation: Distinct */ public final Observable distinctUntilChanged() { return lift(new OperatorDistinctUntilChanged(UtilityFunctions.identity())); @@ -3998,7 +4000,7 @@ public final Observable distinctUntilChanged() { * is distinct from another one or not * @return an Observable that emits those items from the source Observable whose keys are distinct from * those of their immediate predecessors - * @see RxJava wiki: distinctUntilChanged + * @see ReactiveX operators documentation: Distinct */ public final Observable distinctUntilChanged(Func1 keySelector) { return lift(new OperatorDistinctUntilChanged(keySelector)); @@ -4016,7 +4018,7 @@ public final Observable distinctUntilChanged(Func1RxJava wiki: doOnCompleted + * @see ReactiveX operators documentation: Do */ public final Observable doOnCompleted(final Action0 onCompleted) { Observer observer = new Observer() { @@ -4050,7 +4052,7 @@ public final void onNext(T args) { * @param onNotification * the action to invoke for each item emitted by the source Observable * @return the source Observable with the side-effecting behavior applied - * @see RxJava wiki: doOnEach + * @see ReactiveX operators documentation: Do */ public final Observable doOnEach(final Action1> onNotification) { Observer observer = new Observer() { @@ -4086,7 +4088,7 @@ public final void onNext(T v) { * @param observer * the action to invoke for each item emitted by the source Observable * @return the source Observable with the side-effecting behavior applied - * @see RxJava wiki: doOnEach + * @see ReactiveX operators documentation: Do */ public final Observable doOnEach(Observer observer) { return lift(new OperatorDoOnEach(observer)); @@ -4104,7 +4106,7 @@ public final Observable doOnEach(Observer observer) { * @param onError * the action to invoke if the source Observable calls {@code onError} * @return the source Observable with the side-effecting behavior applied - * @see RxJava wiki: doOnError + * @see ReactiveX operators documentation: Do */ public final Observable doOnError(final Action1 onError) { Observer observer = new Observer() { @@ -4138,7 +4140,7 @@ public final void onNext(T args) { * @param onNext * the action to invoke when the source Observable calls {@code onNext} * @return the source Observable with the side-effecting behavior applied - * @see RxJava wiki: doOnEach + * @see ReactiveX operators documentation: Do */ public final Observable doOnNext(final Action1 onNext) { Observer observer = new Observer() { @@ -4171,6 +4173,7 @@ public final void onNext(T args) { * @param onRequest * the action that gets called when an observer requests items from this {@code Observable} * @return the source {@code Observable} modified so as to call this Action when appropriate + * @see ReactiveX operators documentation: Do * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta @@ -4193,7 +4196,7 @@ public final Observable doOnRequest(final Action1 onRequest) { * @param subscribe * the action that gets called when an observer subscribes to this {@code Observable} * @return the source {@code Observable} modified so as to call this Action when appropriate - * @see RxJava wiki: doOnSubscribe + * @see ReactiveX operators documentation: Do */ public final Observable doOnSubscribe(final Action0 subscribe) { return lift(new OperatorDoOnSubscribe(subscribe)); @@ -4215,7 +4218,7 @@ public final Observable doOnSubscribe(final Action0 subscribe) { * @param onTerminate * the action to invoke when the source Observable calls {@code onCompleted} or {@code onError} * @return the source Observable with the side-effecting behavior applied - * @see RxJava wiki: doOnTerminate + * @see ReactiveX operators documentation: Do * @see #finallyDo(Action0) */ public final Observable doOnTerminate(final Action0 onTerminate) { @@ -4254,7 +4257,7 @@ public final void onNext(T args) { * @param unsubscribe * the action that gets called when this {@code Observable} is unsubscribed * @return the source {@code Observable} modified so as to call this Action when appropriate - * @see RxJava wiki: doOnUnsubscribe + * @see ReactiveX operators documentation: Do */ public final Observable doOnUnsubscribe(final Action0 unsubscribe) { return lift(new OperatorDoOnUnsubscribe(unsubscribe)); @@ -4278,7 +4281,7 @@ public final Observable doOnUnsubscribe(final Action0 unsubscribe) { * if {@code index} is greater than or equal to the number of items emitted by the source * Observable, or * if {@code index} is less than 0 - * @see RxJava wiki: elementAt + * @see ReactiveX operators documentation: ElementAt */ public final Observable elementAt(int index) { return lift(new OperatorElementAt(index)); @@ -4302,7 +4305,7 @@ public final Observable elementAt(int index) { * Observable, or the default item if that index is outside the bounds of the source sequence * @throws IndexOutOfBoundsException * if {@code index} is less than 0 - * @see RxJava wiki: elementAtOrDefault + * @see ReactiveX operators documentation: ElementAt */ public final Observable elementAtOrDefault(int index, T defaultValue) { return lift(new OperatorElementAt(index, defaultValue)); @@ -4326,7 +4329,7 @@ public final Observable elementAtOrDefault(int index, T defaultValue) { * the condition to test items emitted by the source Observable * @return an Observable that emits a Boolean that indicates whether any item emitted by the source * Observable satisfies the {@code predicate} - * @see RxJava wiki: exists + * @see ReactiveX operators documentation: Contains */ public final Observable exists(Func1 predicate) { return lift(new OperatorAny(predicate, false)); @@ -4346,7 +4349,7 @@ public final Observable exists(Func1 predicate) { * if it passes the filter * @return an Observable that emits only those items emitted by the source Observable that the filter * evaluates as {@code true} - * @see RxJava wiki: filter + * @see ReactiveX operators documentation: Filter */ public final Observable filter(Func1 predicate) { return lift(new OperatorFilter(predicate)); @@ -4366,7 +4369,7 @@ public final Observable filter(Func1 predicate) { * an {@link Action0} to be invoked when the source Observable finishes * @return an Observable that emits the same items as the source Observable, then invokes the * {@link Action0} - * @see RxJava wiki: finallyDo + * @see ReactiveX operators documentation: Do * @see #doOnTerminate(Action0) */ public final Observable finallyDo(Action0 action) { @@ -4385,8 +4388,7 @@ public final Observable finallyDo(Action0 action) { * * @return an Observable that emits only the very first item emitted by the source Observable, or raises an * {@code NoSuchElementException} if the source Observable is empty - * @see RxJava wiki: first - * @see "MSDN: Observable.firstAsync" + * @see ReactiveX operators documentation: First */ public final Observable first() { return take(1).single(); @@ -4406,8 +4408,7 @@ public final Observable first() { * the condition that an item emitted by the source Observable has to satisfy * @return an Observable that emits only the very first item emitted by the source Observable that satisfies * the {@code predicate}, or raises an {@code NoSuchElementException} if no such items are emitted - * @see RxJava wiki: takeFirst - * @see "MSDN: Observable.firstAsync" + * @see ReactiveX operators documentation: First */ public final Observable first(Func1 predicate) { return takeFirst(predicate).single(); @@ -4427,8 +4428,7 @@ public final Observable first(Func1 predicate) { * the default item to emit if the source Observable doesn't emit anything * @return an Observable that emits only the very first item from the source, or a default item if the * source Observable completes without emitting any items - * @see RxJava wiki: firstOrDefault - * @see "MSDN: Observable.firstOrDefaultAsync" + * @see ReactiveX operators documentation: First */ public final Observable firstOrDefault(T defaultValue) { return take(1).singleOrDefault(defaultValue); @@ -4451,8 +4451,7 @@ public final Observable firstOrDefault(T defaultValue) { * {@code predicate} * @return an Observable that emits only the very first item emitted by the source Observable that satisfies * the {@code predicate}, or a default item if the source Observable emits no such items - * @see RxJava wiki: firstOrDefault - * @see "MSDN: Observable.firstOrDefaultAsync" + * @see ReactiveX operators documentation: First */ public final Observable firstOrDefault(T defaultValue, Func1 predicate) { return takeFirst(predicate).singleOrDefault(defaultValue); @@ -4475,7 +4474,7 @@ public final Observable firstOrDefault(T defaultValue, Func1RxJava wiki: flatMap + * @see ReactiveX operators documentation: FlatMap */ public final Observable flatMap(Func1> func) { return merge(map(func)); @@ -4503,7 +4502,7 @@ public final Observable flatMap(Func1RxJava wiki: flatMap + * @see ReactiveX operators documentation: FlatMap */ public final Observable flatMap( Func1> onNext, @@ -4533,7 +4532,7 @@ public final Observable flatMap( * returns an item to be emitted by the resulting Observable * @return an Observable that emits the results of applying a function to a pair of values emitted by the * source Observable and the collection Observable - * @see RxJava wiki: flatMap + * @see ReactiveX operators documentation: FlatMap */ public final Observable flatMap(final Func1> collectionSelector, final Func2 resultSelector) { @@ -4557,7 +4556,7 @@ public final Observable flatMap(final Func1RxJava wiki: flatMapIterable + * @see ReactiveX operators documentation: FlatMap */ public final Observable flatMapIterable(Func1> collectionSelector) { return merge(map(OperatorMapPair.convertSelector(collectionSelector))); @@ -4585,7 +4584,7 @@ public final Observable flatMapIterable(Func1RxJava wiki: flatMapIterable + * @see ReactiveX operators documentation: FlatMap */ public final Observable flatMapIterable(Func1> collectionSelector, Func2 resultSelector) { @@ -4607,7 +4606,7 @@ public final Observable flatMapIterable(Func1RxJava wiki: onNext, onCompleted, and onError + * @see ReactiveX operators documentation: Subscribe */ public final void forEach(final Action1 onNext) { subscribe(onNext); @@ -4630,7 +4629,7 @@ public final void forEach(final Action1 onNext) { * if {@code onNext} is null, or * if {@code onError} is null, or * if {@code onComplete} is null - * @see RxJava wiki: onNext, onCompleted, and onError + * @see ReactiveX operators documentation: Subscribe */ public final void forEach(final Action1 onNext, final Action1 onError) { subscribe(onNext, onError); @@ -4655,7 +4654,7 @@ public final void forEach(final Action1 onNext, final Action1RxJava wiki: onNext, onCompleted, and onError + * @see ReactiveX operators documentation: Subscribe */ public final void forEach(final Action1 onNext, final Action1 onError, final Action0 onComplete) { subscribe(onNext, onError, onComplete); @@ -4687,7 +4686,7 @@ public final void forEach(final Action1 onNext, final Action1RxJava wiki: groupBy + * @see ReactiveX operators documentation: GroupBy */ public final Observable> groupBy(final Func1 keySelector, final Func1 elementSelector) { return lift(new OperatorGroupBy(keySelector, elementSelector)); @@ -4715,7 +4714,7 @@ public final Observable> groupBy(final Func1RxJava wiki: groupBy + * @see ReactiveX operators documentation: GroupBy */ public final Observable> groupBy(final Func1 keySelector) { return lift(new OperatorGroupBy(keySelector)); @@ -4743,7 +4742,7 @@ public final Observable> groupBy(final Func1RxJava Wiiki: groupJoin + * @see ReactiveX operators documentation: Join */ public final Observable groupJoin(Observable right, Func1> leftDuration, Func1> rightDuration, @@ -4762,7 +4761,7 @@ public final Observable groupJoin(Observable right, Func1 * * @return an empty Observable that only calls {@code onCompleted} or {@code onError}, based on which one is * called by the source Observable - * @see RxJava wiki: ignoreElements + * @see ReactiveX operators documentation: IgnoreElements */ public final Observable ignoreElements() { return filter(UtilityFunctions.alwaysFalse()); @@ -4781,7 +4780,7 @@ public final Observable ignoreElements() { * * * @return an Observable that emits a Boolean - * @see RxJava wiki: isEmpty + * @see ReactiveX operators documentation: Contains */ public final Observable isEmpty() { return lift(new OperatorAny(UtilityFunctions.alwaysTrue(), true)); @@ -4809,7 +4808,7 @@ public final Observable isEmpty() { * overlapping items emitted by the two Observables * @return an Observable that emits items correlating to items emitted by the source Observables that have * overlapping durations - * @see RxJava wiki: join + * @see ReactiveX operators documentation: Join */ public final Observable join(Observable right, Func1> leftDurationSelector, Func1> rightDurationSelector, @@ -4829,8 +4828,7 @@ public final Observable join(Obser * * @return an Observable that emits the last item from the source Observable or notifies observers of an * error - * @see RxJava wiki: last - * @see "MSDN: Observable.lastAsync" + * @see ReactiveX operators documentation: Last */ public final Observable last() { return takeLast(1).single(); @@ -4852,8 +4850,7 @@ public final Observable last() { * {@code NoSuchElementException} if no such items are emitted * @throws IllegalArgumentException * if no items that match the predicate are emitted by the source Observable - * @see RxJava wiki: last - * @see "MSDN: Observable.lastAsync" + * @see ReactiveX operators documentation: Last */ public final Observable last(Func1 predicate) { return filter(predicate).takeLast(1).single(); @@ -4873,8 +4870,7 @@ public final Observable last(Func1 predicate) { * the default item to emit if the source Observable is empty * @return an Observable that emits only the last item emitted by the source Observable, or a default item * if the source Observable is empty - * @see RxJava wiki: lastOrDefault - * @see "MSDN: Observable.lastOrDefaultAsync" + * @see ReactiveX operators documentation: Last */ public final Observable lastOrDefault(T defaultValue) { return takeLast(1).singleOrDefault(defaultValue); @@ -4897,8 +4893,7 @@ public final Observable lastOrDefault(T defaultValue) { * the condition any item emitted by the source Observable has to satisfy * @return an Observable that emits only the last item emitted by the source Observable that satisfies the * given condition, or a default item if no such item is emitted by the source Observable - * @see RxJava wiki: lastOrDefault - * @see "MSDN: Observable.lastOrDefaultAsync" + * @see ReactiveX operators documentation: Last */ public final Observable lastOrDefault(T defaultValue, Func1 predicate) { return filter(predicate).takeLast(1).singleOrDefault(defaultValue); @@ -4923,7 +4918,7 @@ public final Observable lastOrDefault(T defaultValue, Func1RxJava wiki: take + * @see ReactiveX operators documentation: Take */ public final Observable limit(int num) { return take(num); @@ -4943,7 +4938,7 @@ public final Observable limit(int num) { * a function to apply to each item emitted by the Observable * @return an Observable that emits the items from the source Observable, transformed by the specified * function - * @see RxJava wiki: map + * @see ReactiveX operators documentation: Map */ public final Observable map(Func1 func) { return lift(new OperatorMap(func)); @@ -4965,7 +4960,7 @@ private final Observable mapNotification(Func1 on * * @return an Observable that emits items that are the result of materializing the items and notifications * of the source Observable - * @see RxJava wiki: materialize + * @see ReactiveX operators documentation: Materialize */ public final Observable> materialize() { return lift(new OperatorMaterialize()); @@ -4986,7 +4981,7 @@ public final Observable> materialize() { * @param t1 * an Observable to be merged * @return an Observable that emits all of the items emitted by the source Observables - * @see RxJava wiki: merge + * @see ReactiveX operators documentation: Merge */ public final Observable mergeWith(Observable t1) { return merge(this, t1); @@ -5006,7 +5001,7 @@ public final Observable mergeWith(Observable t1) { * the {@link Scheduler} to notify {@link Observer}s on * @return the source Observable modified so that its {@link Observer}s are notified on the specified * {@link Scheduler} - * @see RxJava wiki: observeOn + * @see ReactiveX operators documentation: ObserveOn * @see RxJava Threading Examples * @see #subscribeOn */ @@ -5026,7 +5021,7 @@ public final Observable observeOn(Scheduler scheduler) { * @param klass * the class type to filter the items emitted by the source Observable * @return an Observable that emits items from the source Observable of type {@code klass} - * @see RxJava wiki: ofType + * @see ReactiveX operators documentation: Filter */ public final Observable ofType(final Class klass) { return filter(new Func1() { @@ -5048,7 +5043,7 @@ public final Boolean call(T t) { * * * @return the source Observable modified to buffer items to the extent system resources allow - * @see RxJava wiki: Backpressure + * @see ReactiveX operators documentation: backpressure operators */ public final Observable onBackpressureBuffer() { return lift(new OperatorOnBackpressureBuffer()); @@ -5067,7 +5062,7 @@ public final Observable onBackpressureBuffer() { * * * @return the source Observable modified to buffer items up to the given capacity - * @see RxJava wiki: Backpressure + * @see ReactiveX operators documentation: backpressure operators * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta @@ -5088,7 +5083,7 @@ public final Observable onBackpressureBuffer(long capacity) { * * * @return the source Observable modified to buffer items up to the given capacity - * @see RxJava wiki: Backpressure + * @see ReactiveX operators documentation: backpressure operators * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta @@ -5110,7 +5105,7 @@ public final Observable onBackpressureBuffer(long capacity, Action0 onOverflo * * * @return the source Observable modified to drop {@code onNext} notifications on overflow - * @see RxJava wiki: Backpressure + * @see ReactiveX operators documentation: backpressure operators */ public final Observable onBackpressureDrop() { return lift(new OperatorOnBackpressureDrop()); @@ -5132,7 +5127,7 @@ public final Observable onBackpressureDrop() { * * @param maxQueueLength the maximum number of items the producer can emit without blocking * @return the source Observable modified to block {@code onNext} notifications on overflow - * @see RxJava wiki: Backpressure + * @see ReactiveX operators documentation: backpressure operators * @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) */ @@ -5154,7 +5149,7 @@ public final Observable onBackpressureBlock(int maxQueueLength) { * and doesn't propagate any backpressure requests from downstream. * * @return the source Observable modified to block {@code onNext} notifications on overflow - * @see RxJava wiki: Backpressure + * @see ReactiveX operators documentation: backpressure operators * @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) */ @@ -5190,7 +5185,7 @@ public final Observable onBackpressureBlock() { * a function that returns an Observable that will take over if the source Observable encounters * an error * @return the original Observable, with appropriately modified behavior - * @see RxJava wiki: onErrorResumeNext + * @see ReactiveX operators documentation: Catch */ public final Observable onErrorResumeNext(final Func1> resumeFunction) { return lift(new OperatorOnErrorResumeNextViaFunction(resumeFunction)); @@ -5223,7 +5218,7 @@ public final Observable onErrorResumeNext(final Func1RxJava wiki: onErrorResumeNext + * @see ReactiveX operators documentation: Catch */ public final Observable onErrorResumeNext(final Observable resumeSequence) { return lift(new OperatorOnErrorResumeNextViaObservable(resumeSequence)); @@ -5253,7 +5248,7 @@ public final Observable onErrorResumeNext(final Observable resum * a function that returns an item that the new Observable will emit if the source Observable * encounters an error * @return the original Observable with appropriately modified behavior - * @see RxJava wiki: onErrorReturn + * @see ReactiveX operators documentation: Catch */ public final Observable onErrorReturn(Func1 resumeFunction) { return lift(new OperatorOnErrorReturn(resumeFunction)); @@ -5289,7 +5284,7 @@ public final Observable onErrorReturn(Func1 resumeFun * a function that returns an Observable that will take over if the source Observable encounters * an exception * @return the original Observable, with appropriately modified behavior - * @see RxJava wiki: onExceptionResumeNext + * @see ReactiveX operators documentation: Catch */ public final Observable onExceptionResumeNext(final Observable resumeSequence) { return lift(new OperatorOnExceptionResumeNextViaObservable(resumeSequence)); @@ -5308,7 +5303,7 @@ public final Observable onExceptionResumeNext(final Observable r * * @return a {@link ConnectableObservable} that upon connection causes the source Observable to emit items * to its {@link Observer}s - * @see RxJava wiki: publish + * @see ReactiveX operators documentation: Publish */ public final ConnectableObservable publish() { return OperatorPublish.create(this); @@ -5331,7 +5326,7 @@ public final ConnectableObservable publish() { * causing multiple subscriptions to the source sequence. Subscribers to the given source will * receive all notifications of the source from the time of the subscription forward. * @return an Observable that emits the results of invoking the selector on the items emitted by a {@link ConnectableObservable} that shares a single subscription to the underlying sequence - * @see RxJava wiki: publish + * @see ReactiveX operators documentation: Publish */ public final Observable publish(Func1, ? extends Observable> selector) { return OperatorPublish.create(this, selector); @@ -5363,7 +5358,7 @@ public final Observable publish(Func1, ? extends Ob * the source Observable * @throws IllegalArgumentException * if the source Observable emits no items - * @see RxJava wiki: reduce + * @see ReactiveX operators documentation: Reduce * @see Wikipedia: Fold (higher-order function) */ public final Observable reduce(Func2 accumulator) { @@ -5402,7 +5397,7 @@ public final Observable reduce(Func2 accumulator) { * result of which will be used in the next accumulator call * @return an Observable that emits a single item that is the result of accumulating the output from the * items emitted by the source Observable - * @see RxJava wiki: reduce + * @see ReactiveX operators documentation: Reduce * @see Wikipedia: Fold (higher-order function) */ public final Observable reduce(R initialValue, Func2 accumulator) { @@ -5419,7 +5414,7 @@ public final Observable reduce(R initialValue, Func2 acc * * * @return an Observable that emits the items emitted by the source Observable repeatedly and in sequence - * @see RxJava wiki: repeat + * @see ReactiveX operators documentation: Repeahttp://reactivex.io/documentation/operators/create.htmlt */ public final Observable repeat() { return OnSubscribeRedo.repeat(this); @@ -5438,7 +5433,7 @@ public final Observable repeat() { * @param scheduler * the Scheduler to emit the items on * @return an Observable that emits the items emitted by the source Observable repeatedly and in sequence - * @see RxJava wiki: repeat + * @see ReactiveX operators documentation: Repeat */ public final Observable repeat(Scheduler scheduler) { return OnSubscribeRedo.repeat(this, scheduler); @@ -5461,7 +5456,7 @@ public final Observable repeat(Scheduler scheduler) { * {@code count} times * @throws IllegalArgumentException * if {@code count} is less than zero - * @see RxJava wiki: repeat + * @see ReactiveX operators documentation: Repeat */ public final Observable repeat(final long count) { return OnSubscribeRedo.repeat(this, count); @@ -5484,7 +5479,7 @@ public final Observable repeat(final long count) { * the {@link Scheduler} to emit the items on * @return an Observable that repeats the sequence of items emitted by the source Observable at most * {@code count} times on a particular Scheduler - * @see RxJava Wiki: repeat() + * @see ReactiveX operators documentation: Repeat */ public final Observable repeat(final long count, Scheduler scheduler) { return OnSubscribeRedo.repeat(this, count, scheduler); @@ -5509,7 +5504,7 @@ public final Observable repeat(final long count, Scheduler scheduler) { * @param scheduler * the {@link Scheduler} to emit the items on * @return the source Observable modified with repeat logic - * @see RxJava Wiki: repeatWhen() + * @see ReactiveX operators documentation: Repeat */ public final Observable repeatWhen(final Func1, ? extends Observable> notificationHandler, Scheduler scheduler) { Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { @@ -5543,7 +5538,7 @@ public Void call(Notification notification) { * @param notificationHandler * receives an Observable of notifications with which a user can complete or error, aborting the repeat. * @return the source Observable modified with repeat logic - * @see RxJava Wiki: repeatWhen() + * @see ReactiveX operators documentation: Repeat */ public final Observable repeatWhen(final Func1, ? extends Observable> notificationHandler) { Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { @@ -5578,7 +5573,7 @@ public Void call(Notification notification) { * * @return a {@link ConnectableObservable} that upon connection causes the source Observable to emit its * items to its {@link Observer}s - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay() { return new OperatorMulticast(this, new Func0>() { @@ -5612,7 +5607,7 @@ public final ConnectableObservable replay() { * causing multiple subscriptions to the Observable * @return an Observable that emits items that are the results of invoking the selector on a * {@link ConnectableObservable} that shares a single subscription to the source Observable - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector) { return create(new OnSubscribeMulticastSelector(this, new Func0>() { @@ -5648,7 +5643,7 @@ public final Subject call() { * @return an Observable that emits items that are the results of invoking the selector on items emitted by * a {@link ConnectableObservable} that shares a single subscription to the source Observable * replaying no more than {@code bufferSize} items - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize) { return create(new OnSubscribeMulticastSelector(this, new Func0>() { @@ -5689,7 +5684,7 @@ public final Subject call() { * a {@link ConnectableObservable} that shares a single subscription to the source Observable, and * replays no more than {@code bufferSize} items that were emitted within the window defined by * {@code time} - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, int bufferSize, long time, TimeUnit unit) { return replay(selector, bufferSize, time, unit, Schedulers.computation()); @@ -5729,7 +5724,7 @@ public final Observable replay(Func1, ? extends Obs * {@code time} * @throws IllegalArgumentException * if {@code bufferSize} is less than zero - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler) { if (bufferSize < 0) { @@ -5770,7 +5765,7 @@ public final Subject call() { * @return an Observable that emits items that are the results of invoking the selector on items emitted by * a {@link ConnectableObservable} that shares a single subscription to the source Observable, * replaying no more than {@code bufferSize} notifications - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize, final Scheduler scheduler) { return create(new OnSubscribeMulticastSelector(this, new Func0>() { @@ -5808,7 +5803,7 @@ public final Subject call() { * @return an Observable that emits items that are the results of invoking the selector on items emitted by * a {@link ConnectableObservable} that shares a single subscription to the source Observable, * replaying all items that were emitted within the window defined by {@code time} - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, long time, TimeUnit unit) { return replay(selector, time, unit, Schedulers.computation()); @@ -5843,7 +5838,7 @@ public final Observable replay(Func1, ? extends Obs * @return an Observable that emits items that are the results of invoking the selector on items emitted by * a {@link ConnectableObservable} that shares a single subscription to the source Observable, * replaying all items that were emitted within the window defined by {@code time} - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final long time, final TimeUnit unit, final Scheduler scheduler) { return create(new OnSubscribeMulticastSelector(this, new Func0>() { @@ -5878,7 +5873,7 @@ public final Subject call() { * @return an Observable that emits items that are the results of invoking the selector on items emitted by * a {@link ConnectableObservable} that shares a single subscription to the source Observable, * replaying all items - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final Scheduler scheduler) { return create(new OnSubscribeMulticastSelector(this, new Func0>() { @@ -5909,7 +5904,7 @@ public final Subject call() { * the buffer size that limits the number of items that can be replayed * @return a {@link ConnectableObservable} that shares a single subscription to the source Observable and * replays at most {@code bufferSize} items emitted by that Observable - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize) { return new OperatorMulticast(this, new Func0>() { @@ -5947,7 +5942,7 @@ public final ConnectableObservable replay(final int bufferSize) { * @return a {@link ConnectableObservable} that shares a single subscription to the source Observable and * replays at most {@code bufferSize} items that were emitted during the window defined by * {@code time} - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(int bufferSize, long time, TimeUnit unit) { return replay(bufferSize, time, unit, Schedulers.computation()); @@ -5982,7 +5977,7 @@ public final ConnectableObservable replay(int bufferSize, long time, TimeUnit * {@code time} * @throws IllegalArgumentException * if {@code bufferSize} is less than zero - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler) { if (bufferSize < 0) { @@ -6020,7 +6015,7 @@ public final ConnectableObservable replay(final int bufferSize, final long ti * the scheduler on which the Observers will observe the emitted items * @return a {@link ConnectableObservable} that shares a single subscription to the source Observable and * replays at most {@code bufferSize} items that were emitted by the Observable - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize, final Scheduler scheduler) { return new OperatorMulticast(this, new Func0>() { @@ -6055,7 +6050,7 @@ public final ConnectableObservable replay(final int bufferSize, final Schedul * the time unit of {@code time} * @return a {@link ConnectableObservable} that shares a single subscription to the source Observable and * replays the items that were emitted during the window defined by {@code time} - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(long time, TimeUnit unit) { return replay(time, unit, Schedulers.computation()); @@ -6085,7 +6080,7 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * the Scheduler that is the time source for the window * @return a {@link ConnectableObservable} that shares a single subscription to the source Observable and * replays the items that were emitted during the window defined by {@code time} - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final long time, final TimeUnit unit, final Scheduler scheduler) { return new OperatorMulticast(this, new Func0>() { @@ -6119,7 +6114,7 @@ public final ConnectableObservable replay(final long time, final TimeUnit uni * @return a {@link ConnectableObservable} that shares a single subscription to the source Observable that * will replay all of its items and notifications to any future {@link Observer} on the given * {@link Scheduler} - * @see RxJava wiki: replay + * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final Scheduler scheduler) { return new OperatorMulticast(this, new Func0>() { @@ -6151,7 +6146,7 @@ public final ConnectableObservable replay(final Scheduler scheduler) { * * * @return the source Observable modified with retry logic - * @see RxJava wiki: retry + * @see ReactiveX operators documentation: Retry */ public final Observable retry() { return OnSubscribeRedo.retry(this); @@ -6179,7 +6174,7 @@ public final Observable retry() { * @param count * number of retry attempts before failing * @return the source Observable modified with retry logic - * @see RxJava wiki: retry + * @see ReactiveX operators documentation: Retry */ public final Observable retry(final long count) { return OnSubscribeRedo.retry(this, count); @@ -6200,7 +6195,7 @@ public final Observable retry(final long count) { * and retry count * @return the source Observable modified with retry logic * @see #retry() - * @see RxJava Wiki: retry() + * @see ReactiveX operators documentation: Retry */ public final Observable retry(Func2 predicate) { return nest().lift(new OperatorRetryWithPredicate(predicate)); @@ -6252,7 +6247,7 @@ public final Observable retry(Func2 predicate) { * receives an Observable of notifications with which a user can complete or error, aborting the * retry * @return the source Observable modified with retry logic - * @see RxJava Wiki: retryWhen() + * @see ReactiveX operators documentation: Retry */ public final Observable retryWhen(final Func1, ? extends Observable> notificationHandler) { Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { @@ -6290,7 +6285,7 @@ public Throwable call(Notification notification) { * @param scheduler * the {@link Scheduler} on which to subscribe to the source Observable * @return the source Observable modified with retry logic - * @see RxJava Wiki: retryWhen() + * @see ReactiveX operators documentation: Retry */ public final Observable retryWhen(final Func1, ? extends Observable> notificationHandler, Scheduler scheduler) { Func1>, ? extends Observable> dematerializedNotificationHandler = new Func1>, Observable>() { @@ -6325,7 +6320,7 @@ public Throwable call(Notification notification) { * the {@link TimeUnit} in which {@code period} is defined * @return an Observable that emits the results of sampling the items emitted by the source Observable at * the specified time interval - * @see RxJava wiki: sample + * @see ReactiveX operators documentation: Sample * @see RxJava wiki: Backpressure * @see #throttleLast(long, TimeUnit) */ @@ -6353,7 +6348,7 @@ public final Observable sample(long period, TimeUnit unit) { * the {@link Scheduler} to use when sampling * @return an Observable that emits the results of sampling the items emitted by the source Observable at * the specified time interval - * @see RxJava wiki: sample + * @see ReactiveX operators documentation: Sample * @see RxJava wiki: Backpressure * @see #throttleLast(long, TimeUnit, Scheduler) */ @@ -6379,7 +6374,7 @@ public final Observable sample(long period, TimeUnit unit, Scheduler schedule * the Observable to use for sampling the source Observable * @return an Observable that emits the results of sampling the items emitted by this Observable whenever * the {@code sampler} Observable emits an item or completes - * @see RxJava wiki: sample + * @see ReactiveX operators documentation: Sample * @see RxJava wiki: Backpressure */ public final Observable sample(Observable sampler) { @@ -6405,7 +6400,7 @@ public final Observable sample(Observable sampler) { * result will be emitted to {@link Observer}s via {@link Observer#onNext onNext} and used in the * next accumulator call * @return an Observable that emits the results of each call to the accumulator function - * @see RxJava wiki: scan + * @see ReactiveX operators documentation: Scan */ public final Observable scan(Func2 accumulator) { return lift(new OperatorScan(accumulator)); @@ -6436,7 +6431,7 @@ public final Observable scan(Func2 accumulator) { * next accumulator call * @return an Observable that emits {@code initialValue} followed by the results of each call to the * accumulator function - * @see RxJava wiki: scan + * @see ReactiveX operators documentation: Scan */ public final Observable scan(R initialValue, Func2 accumulator) { return lift(new OperatorScan(initialValue, accumulator)); @@ -6460,7 +6455,7 @@ public final Observable scan(R initialValue, Func2 accum * * @return an {@link Observable} that is guaranteed to be well-behaved and to make only serialized calls to * its observers - * @see RxJava wiki: serialize + * @see ReactiveX operators documentation: Serialize */ public final Observable serialize() { return lift(new OperatorSerialize()); @@ -6485,7 +6480,7 @@ public final Observable serialize() { * * @return an {@code Observable} that upon connection causes the source {@code Observable} to emit items * to its {@link Observer}s - * @see RxJava wiki: refCount + * @see ReactiveX operators documentation: RefCount */ public final Observable share() { return publish().refCount(); @@ -6507,8 +6502,7 @@ public final Observable share() { * if the source emits more than one item * @throws NoSuchElementException * if the source emits no items - * @see RxJava wiki: single - * @see "MSDN: Observable.singleAsync" + * @see ReactiveX operators documentation: First */ public final Observable single() { return lift(new OperatorSingle()); @@ -6534,8 +6528,7 @@ public final Observable single() { * if the source Observable emits more than one item that matches the predicate * @throws NoSuchElementException * if the source Observable emits no item that matches the predicate - * @see RxJava wiki: single - * @see "MSDN: Observable.singleAsync" + * @see ReactiveX operators documentation: First */ public final Observable single(Func1 predicate) { return filter(predicate).single(); @@ -6558,8 +6551,7 @@ public final Observable single(Func1 predicate) { * the source Observable is empty * @throws IllegalArgumentException * if the source Observable emits more than one item - * @see RxJava wiki: single - * @see "MSDN: Observable.singleOrDefaultAsync" + * @see ReactiveX operators documentation: First */ public final Observable singleOrDefault(T defaultValue) { return lift(new OperatorSingle(defaultValue)); @@ -6585,8 +6577,7 @@ public final Observable singleOrDefault(T defaultValue) { * predicate, or the default item if no emitted item matches the predicate * @throws IllegalArgumentException * if the source Observable emits more than one item that matches the predicate - * @see RxJava wiki: single - * @see "MSDN: Observable.singleOrDefaultAsync" + * @see ReactiveX operators documentation: First */ public final Observable singleOrDefault(T defaultValue, Func1 predicate) { return filter(predicate).singleOrDefault(defaultValue); @@ -6606,7 +6597,7 @@ public final Observable singleOrDefault(T defaultValue, Func1RxJava wiki: skip + * @see ReactiveX operators documentation: Skip */ public final Observable skip(int num) { return lift(new OperatorSkip(num)); @@ -6628,7 +6619,7 @@ public final Observable skip(int num) { * the time unit of {@code time} * @return an Observable that skips values emitted by the source Observable before the time window defined * by {@code time} elapses and the emits the remainder - * @see RxJava wiki: skip + * @see ReactiveX operators documentation: Skip */ public final Observable skip(long time, TimeUnit unit) { return skip(time, unit, Schedulers.computation()); @@ -6652,7 +6643,7 @@ public final Observable skip(long time, TimeUnit unit) { * the {@link Scheduler} on which the timed wait happens * @return an Observable that skips values emitted by the source Observable before the time window defined * by {@code time} and {@code scheduler} elapses, and then emits the remainder - * @see RxJava wiki: skip + * @see ReactiveX operators documentation: Skip */ public final Observable skip(long time, TimeUnit unit, Scheduler scheduler) { return lift(new OperatorSkipTimed(time, unit, scheduler)); @@ -6678,7 +6669,7 @@ public final Observable skip(long time, TimeUnit unit, Scheduler scheduler) { * at the end * @throws IndexOutOfBoundsException * if {@code count} is less than zero - * @see RxJava wiki: skipLast + * @see ReactiveX operators documentation: SkipLast */ public final Observable skipLast(int count) { return lift(new OperatorSkipLast(count)); @@ -6702,7 +6693,7 @@ public final Observable skipLast(int count) { * the time unit of {@code time} * @return an Observable that drops those items emitted by the source Observable in a time window before the * source completes defined by {@code time} - * @see RxJava wiki: skipLast + * @see ReactiveX operators documentation: SkipLast */ public final Observable skipLast(long time, TimeUnit unit) { return skipLast(time, unit, Schedulers.computation()); @@ -6728,7 +6719,7 @@ public final Observable skipLast(long time, TimeUnit unit) { * the scheduler used as the time source * @return an Observable that drops those items emitted by the source Observable in a time window before the * source completes defined by {@code time} and {@code scheduler} - * @see RxJava wiki: skipLast + * @see ReactiveX operators documentation: SkipLast */ public final Observable skipLast(long time, TimeUnit unit, Scheduler scheduler) { return lift(new OperatorSkipLastTimed(time, unit, scheduler)); @@ -6749,7 +6740,7 @@ public final Observable skipLast(long time, TimeUnit unit, Scheduler schedule * to be mirrored by the resulting Observable * @return an Observable that skips items from the source Observable until the second Observable emits an * item, then emits the remaining items - * @see RxJava wiki: skipUntil + * @see ReactiveX operators documentation: SkipUntil */ public final Observable skipUntil(Observable other) { return lift(new OperatorSkipUntil(other)); @@ -6769,7 +6760,7 @@ public final Observable skipUntil(Observable other) { * a function to test each item emitted from the source Observable * @return an Observable that begins emitting items emitted by the source Observable when the specified * predicate becomes false - * @see RxJava wiki: skipWhile + * @see ReactiveX operators documentation: SkipWhile */ public final Observable skipWhile(Func1 predicate) { return lift(new OperatorSkipWhile(OperatorSkipWhile.toPredicate2(predicate))); @@ -6789,7 +6780,7 @@ public final Observable skipWhile(Func1 predicate) { * an Observable that contains the items you want the modified Observable to emit first * @return an Observable that emits the items in the specified {@link Observable} and then emits the items * emitted by the source Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(Observable values) { return concat(values, this); @@ -6809,7 +6800,7 @@ public final Observable startWith(Observable values) { * an Iterable that contains the items you want the modified Observable to emit first * @return an Observable that emits the items in the specified {@link Iterable} and then emits the items * emitted by the source Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(Iterable values) { return concat(Observable. from(values), this); @@ -6829,7 +6820,7 @@ public final Observable startWith(Iterable values) { * the item to emit * @return an Observable that emits the specified item before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1) { return concat(just(t1), this); @@ -6851,7 +6842,7 @@ public final Observable startWith(T t1) { * the second item to emit * @return an Observable that emits the specified items before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2) { return concat(just(t1, t2), this); @@ -6875,7 +6866,7 @@ public final Observable startWith(T t1, T t2) { * the third item to emit * @return an Observable that emits the specified items before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2, T t3) { return concat(just(t1, t2, t3), this); @@ -6901,7 +6892,7 @@ public final Observable startWith(T t1, T t2, T t3) { * the fourth item to emit * @return an Observable that emits the specified items before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2, T t3, T t4) { return concat(just(t1, t2, t3, t4), this); @@ -6929,7 +6920,7 @@ public final Observable startWith(T t1, T t2, T t3, T t4) { * the fifth item to emit * @return an Observable that emits the specified items before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2, T t3, T t4, T t5) { return concat(just(t1, t2, t3, t4, t5), this); @@ -6959,7 +6950,7 @@ public final Observable startWith(T t1, T t2, T t3, T t4, T t5) { * the sixth item to emit * @return an Observable that emits the specified items before it begins to emit items emitted * by the source Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6) { return concat(just(t1, t2, t3, t4, t5, t6), this); @@ -6991,7 +6982,7 @@ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6) { * the seventh item to emit * @return an Observable that emits the specified items before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7) { return concat(just(t1, t2, t3, t4, t5, t6, t7), this); @@ -7025,7 +7016,7 @@ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7) { * the eighth item to emit * @return an Observable that emits the specified items before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8) { return concat(just(t1, t2, t3, t4, t5, t6, t7, t8), this); @@ -7061,7 +7052,7 @@ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T * the ninth item to emit * @return an Observable that emits the specified items before it begins to emit items emitted by the source * Observable - * @see RxJava wiki: startWith + * @see ReactiveX operators documentation: StartWith */ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8, T t9) { return concat(just(t1, t2, t3, t4, t5, t6, t7, t8, t9), this); @@ -7078,6 +7069,7 @@ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T * the Observable has finished sending them * @throws OnErrorNotImplementedException * if the Observable tries to call {@code onError} + * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe() { return subscribe(new Subscriber() { @@ -7115,7 +7107,7 @@ public final void onNext(T args) { * if {@code onNext} is null * @throws OnErrorNotImplementedException * if the Observable tries to call {@code onError} - * @see RxJava wiki: onNext, onCompleted, and onError + * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(final Action1 onNext) { if (onNext == null) { @@ -7157,7 +7149,7 @@ public final void onNext(T args) { * Observable * @return a {@link Subscription} reference with which the {@link Observer} can stop receiving items before * the Observable has finished sending them - * @see RxJava wiki: onNext, onCompleted, and onError + * @see ReactiveX operators documentation: Subscribe * @throws IllegalArgumentException * if {@code onNext} is null, or * if {@code onError} is null @@ -7212,7 +7204,7 @@ public final void onNext(T args) { * if {@code onNext} is null, or * if {@code onError} is null, or * if {@code onComplete} is null - * @see RxJava wiki: onNext, onCompleted, and onError + * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(final Action1 onNext, final Action1 onError, final Action0 onComplete) { if (onNext == null) { @@ -7257,7 +7249,7 @@ public final void onNext(T args) { * the Observer that will handle emissions and notifications from the Observable * @return a {@link Subscription} reference with which the {@link Observer} can stop receiving items before * the Observable has completed - * @see RxJava wiki: onNext, onCompleted, and onError + * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(final Observer observer) { return subscribe(new Subscriber() { @@ -7342,7 +7334,7 @@ public final Subscription unsafeSubscribe(Subscriber subscriber) { * receive their notifications. *

* For more information see the - * RxJava wiki. + * ReactiveX documentation. *

*
Scheduler:
*
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
@@ -7360,6 +7352,7 @@ public final Subscription unsafeSubscribe(Subscriber subscriber) { * if the {@link Subscriber}'s {@code onError} method is null * @throws RuntimeException * if the {@link Subscriber}'s {@code onError} method itself threw a {@code Throwable} + * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(Subscriber subscriber) { // validate and proceed @@ -7427,7 +7420,7 @@ public final Subscription subscribe(Subscriber subscriber) { * the {@link Scheduler} to perform subscription actions on * @return the source Observable modified so that its subscriptions happen on the * specified {@link Scheduler} - * @see RxJava wiki: subscribeOn + * @see ReactiveX operators documentation: SubscribeOn * @see RxJava Threading Examples * @see #observeOn */ @@ -7450,7 +7443,7 @@ public final Observable subscribeOn(Scheduler scheduler) { * 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 RxJava wiki: switchMap + * @see ReactiveX operators documentation: FlatMap */ public final Observable switchMap(Func1> func) { return switchOnNext(map(func)); @@ -7473,7 +7466,7 @@ public final Observable switchMap(Func1RxJava wiki: take + * @see ReactiveX operators documentation: Take */ public final Observable take(final int num) { return lift(new OperatorTake(num)); @@ -7494,7 +7487,7 @@ public final Observable take(final int num) { * @param unit * the time unit of {@code time} * @return an Observable that emits those items emitted by the source Observable before the time runs out - * @see RxJava wiki: take + * @see ReactiveX operators documentation: Take */ public final Observable take(long time, TimeUnit unit) { return take(time, unit, Schedulers.computation()); @@ -7518,7 +7511,7 @@ public final Observable take(long time, TimeUnit unit) { * the Scheduler used for time source * @return an Observable that emits those items emitted by the source Observable before the time runs out, * according to the specified Scheduler - * @see RxJava wiki: take + * @see ReactiveX operators documentation: Take */ public final Observable take(long time, TimeUnit unit, Scheduler scheduler) { return lift(new OperatorTakeTimed(time, unit, scheduler)); @@ -7539,8 +7532,7 @@ public final Observable take(long time, TimeUnit unit, Scheduler scheduler) { * @return an Observable that emits only the very first item emitted by the source Observable that satisfies * the given condition, or that completes without emitting anything if the source Observable * completes without emitting a single condition-satisfying item - * @see RxJava wiki: takeFirst - * @see "MSDN: Observable.firstAsync" + * @see ReactiveX operators documentation: First */ public final Observable takeFirst(Func1 predicate) { return filter(predicate).take(1); @@ -7561,7 +7553,7 @@ public final Observable takeFirst(Func1 predicate) { * @return an Observable that emits only the last {@code count} items emitted by the source Observable * @throws IndexOutOfBoundsException * if {@code count} is less than zero - * @see RxJava wiki: takeLast + * @see ReactiveX operators documentation: TakeLast */ public final Observable takeLast(final int count) { return lift(new OperatorTakeLast(count)); @@ -7585,7 +7577,7 @@ public final Observable takeLast(final int count) { * the time unit of {@code time} * @return an Observable that emits at most {@code count} items from the source Observable that were emitted * in a specified window of time before the Observable completed - * @see RxJava wiki: takeLast + * @see ReactiveX operators documentation: TakeLast */ public final Observable takeLast(int count, long time, TimeUnit unit) { return takeLast(count, time, unit, Schedulers.computation()); @@ -7615,7 +7607,7 @@ public final Observable takeLast(int count, long time, TimeUnit unit) { * provided by the given {@code scheduler} * @throws IndexOutOfBoundsException * if {@code count} is less than zero - * @see RxJava wiki: takeLast + * @see ReactiveX operators documentation: TakeLast */ public final Observable takeLast(int count, long time, TimeUnit unit, Scheduler scheduler) { return lift(new OperatorTakeLastTimed(count, time, unit, scheduler)); @@ -7637,7 +7629,7 @@ public final Observable takeLast(int count, long time, TimeUnit unit, Schedul * the time unit of {@code time} * @return an Observable that emits the items from the source Observable that were emitted in the window of * time before the Observable completed specified by {@code time} - * @see RxJava wiki: takeLast + * @see ReactiveX operators documentation: TakeLast */ public final Observable takeLast(long time, TimeUnit unit) { return takeLast(time, unit, Schedulers.computation()); @@ -7663,7 +7655,7 @@ public final Observable takeLast(long time, TimeUnit unit) { * @return an Observable that emits the items from the source Observable that were emitted in the window of * time before the Observable completed specified by {@code time}, where the timing information is * provided by {@code scheduler} - * @see RxJava wiki: takeLast + * @see ReactiveX operators documentation: TakeLast */ public final Observable takeLast(long time, TimeUnit unit, Scheduler scheduler) { return lift(new OperatorTakeLastTimed(time, unit, scheduler)); @@ -7683,7 +7675,7 @@ public final Observable takeLast(long time, TimeUnit unit, Scheduler schedule * the number of items to emit in the list * @return an Observable that emits a single list containing the last {@code count} elements emitted by the * source Observable - * @see RxJava wiki: takeLastBuffer + * @see ReactiveX operators documentation: TakeLast */ public final Observable> takeLastBuffer(int count) { return takeLast(count).toList(); @@ -7708,7 +7700,7 @@ public final Observable> takeLastBuffer(int count) { * @return an Observable that emits a single List containing at most {@code count} items emitted by the * source Observable during the time window defined by {@code time} before the source Observable * completed - * @see RxJava wiki: takeLastBuffer + * @see ReactiveX operators documentation: TakeLast */ public final Observable> takeLastBuffer(int count, long time, TimeUnit unit) { return takeLast(count, time, unit).toList(); @@ -7736,7 +7728,7 @@ public final Observable> takeLastBuffer(int count, long time, TimeUnit u * @return an Observable that emits a single List containing at most {@code count} items emitted by the * source Observable during the time window defined by {@code time} before the source Observable * completed - * @see RxJava wiki: takeLastBuffer + * @see ReactiveX operators documentation: TakeLast */ public final Observable> takeLastBuffer(int count, long time, TimeUnit unit, Scheduler scheduler) { return takeLast(count, time, unit, scheduler).toList(); @@ -7758,7 +7750,7 @@ public final Observable> takeLastBuffer(int count, long time, TimeUnit u * the time unit of {@code time} * @return an Observable that emits a single List containing the items emitted by the source Observable * during the time window defined by {@code time} before the source Observable completed - * @see RxJava wiki: takeLastBuffer + * @see ReactiveX operators documentation: TakeLast */ public final Observable> takeLastBuffer(long time, TimeUnit unit) { return takeLast(time, unit).toList(); @@ -7784,7 +7776,7 @@ public final Observable> takeLastBuffer(long time, TimeUnit unit) { * @return an Observable that emits a single List containing the items emitted by the source Observable * during the time window defined by {@code time} before the source Observable completed, where the * timing information is provided by {@code scheduler} - * @see RxJava wiki: takeLastBuffer + * @see ReactiveX operators documentation: TakeLast */ public final Observable> takeLastBuffer(long time, TimeUnit unit, Scheduler scheduler) { return takeLast(time, unit, scheduler).toList(); @@ -7806,7 +7798,7 @@ public final Observable> takeLastBuffer(long time, TimeUnit unit, Schedu * @param * the type of items emitted by {@code other} * @return an Observable that emits the items emitted by the source Observable until such time as {@code other} emits its first item - * @see RxJava wiki: takeUntil + * @see ReactiveX operators documentation: TakeUntil */ public final Observable takeUntil(Observable other) { return lift(new OperatorTakeUntil(other)); @@ -7826,7 +7818,7 @@ public final Observable takeUntil(Observable other) { * a function that evaluates an item emitted by the source Observable and returns a Boolean * @return an Observable that emits the items from the source Observable so long as each item satisfies the * condition defined by {@code predicate}, then completes - * @see RxJava wiki: takeWhile + * @see ReactiveX operators documentation: TakeWhile */ public final Observable takeWhile(final Func1 predicate) { return lift(new OperatorTakeWhile(predicate)); @@ -7852,7 +7844,7 @@ public final Observable takeWhile(final Func1 predicate) * @param unit * the unit of time of {@code windowDuration} * @return an Observable that performs the throttle operation - * @see RxJava wiki: throttleFirst + * @see ReactiveX operators documentation: Sample * @see RxJava wiki: Backpressure */ public final Observable throttleFirst(long windowDuration, TimeUnit unit) { @@ -7882,7 +7874,7 @@ public final Observable throttleFirst(long windowDuration, TimeUnit unit) { * the {@link Scheduler} to use internally to manage the timers that handle timeout for each * event * @return an Observable that performs the throttle operation - * @see RxJava wiki: throttleFirst + * @see ReactiveX operators documentation: Sample * @see RxJava wiki: Backpressure */ public final Observable throttleFirst(long skipDuration, TimeUnit unit, Scheduler scheduler) { @@ -7910,7 +7902,7 @@ public final Observable throttleFirst(long skipDuration, TimeUnit unit, Sched * @param unit * the unit of time of {@code intervalDuration} * @return an Observable that performs the throttle operation - * @see RxJava wiki: throttleLast + * @see ReactiveX operators documentation: Sample * @see RxJava wiki: Backpressure * @see #sample(long, TimeUnit) */ @@ -7942,7 +7934,7 @@ public final Observable throttleLast(long intervalDuration, TimeUnit unit) { * the {@link Scheduler} to use internally to manage the timers that handle timeout for each * event * @return an Observable that performs the throttle operation - * @see RxJava wiki: throttleLast + * @see ReactiveX operators documentation: Sample * @see RxJava wiki: Backpressure * @see #sample(long, TimeUnit, Scheduler) */ @@ -7980,7 +7972,7 @@ public final Observable throttleLast(long intervalDuration, TimeUnit unit, Sc * @param unit * the {@link TimeUnit} of {@code timeout} * @return an Observable that filters out items that are too quickly followed by newer items - * @see RxJava wiki: throttleWithTimeout + * @see ReactiveX operators documentation: Debounce * @see RxJava wiki: Backpressure * @see #debounce(long, TimeUnit) */ @@ -8022,7 +8014,7 @@ public final Observable throttleWithTimeout(long timeout, TimeUnit unit) { * the {@link Scheduler} to use internally to manage the timers that handle the timeout for each * item * @return an Observable that filters out items that are too quickly followed by newer items - * @see RxJava wiki: throttleWithTimeout + * @see ReactiveX operators documentation: Debounce * @see RxJava wiki: Backpressure * @see #debounce(long, TimeUnit, Scheduler) */ @@ -8041,7 +8033,7 @@ public final Observable throttleWithTimeout(long timeout, TimeUnit unit, Sche *
* * @return an Observable that emits time interval information items - * @see RxJava wiki: timeInterval + * @see ReactiveX operators documentation: TimeInterval */ public final Observable> timeInterval() { return timeInterval(Schedulers.immediate()); @@ -8060,7 +8052,7 @@ public final Observable> timeInterval() { * @param scheduler * the {@link Scheduler} used to compute time intervals * @return an Observable that emits time interval information items - * @see RxJava wiki: timeInterval + * @see ReactiveX operators documentation: TimeInterval */ public final Observable> timeInterval(Scheduler scheduler) { return lift(new OperatorTimeInterval(scheduler)); @@ -8091,7 +8083,7 @@ public final Observable> timeInterval(Scheduler scheduler) { * @return an Observable that mirrors the source Observable, but notifies observers of a * {@code TimeoutException} if either the first item or any subsequent item doesn't arrive within * the time windows specified by the timeout selectors - * @see RxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(Func0> firstTimeoutSelector, Func1> timeoutSelector) { return timeout(firstTimeoutSelector, timeoutSelector, null); @@ -8126,7 +8118,7 @@ public final Observable timeout(Func0> firstTi * within time windows defined by the timeout selectors * @throws NullPointerException * if {@code timeoutSelector} is null - * @see RxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(Func0> firstTimeoutSelector, Func1> timeoutSelector, Observable other) { if (timeoutSelector == null) { @@ -8157,7 +8149,7 @@ public final Observable timeout(Func0> firstTi * @return an Observable that mirrors the source Observable, but notifies observers of a * {@code TimeoutException} if an item emitted by the source Observable takes longer to arrive than * the time window defined by the selector for the previously emitted item - * @see RxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(Func1> timeoutSelector) { return timeout(null, timeoutSelector, null); @@ -8187,7 +8179,7 @@ public final Observable timeout(Func1> * @return an Observable that mirrors the source Observable, but switches to mirroring a fallback Observable * if an item emitted by the source Observable takes longer to arrive than the time window defined * by the selector for the previously emitted item - * @see RxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(Func1> timeoutSelector, Observable other) { return timeout(null, timeoutSelector, other); @@ -8210,7 +8202,7 @@ public final Observable timeout(Func1> * the unit of time that applies to the {@code timeout} argument. * @return the source Observable modified to notify observers of a {@code TimeoutException} in case of a * timeout - * @see RxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(long timeout, TimeUnit timeUnit) { return timeout(timeout, timeUnit, null, Schedulers.computation()); @@ -8234,7 +8226,7 @@ public final Observable timeout(long timeout, TimeUnit timeUnit) { * @param other * the fallback Observable to use in case of a timeout * @return the source Observable modified to switch to the fallback Observable in case of a timeout - * @see RxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(long timeout, TimeUnit timeUnit, Observable other) { return timeout(timeout, timeUnit, other, Schedulers.computation()); @@ -8261,7 +8253,7 @@ public final Observable timeout(long timeout, TimeUnit timeUnit, ObservableRxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(long timeout, TimeUnit timeUnit, Observable other, Scheduler scheduler) { return lift(new OperatorTimeout(timeout, timeUnit, other, scheduler)); @@ -8287,7 +8279,7 @@ public final Observable timeout(long timeout, TimeUnit timeUnit, ObservableRxJava wiki: timeout + * @see ReactiveX operators documentation: Timeout */ public final Observable timeout(long timeout, TimeUnit timeUnit, Scheduler scheduler) { return timeout(timeout, timeUnit, null, scheduler); @@ -8304,7 +8296,7 @@ public final Observable timeout(long timeout, TimeUnit timeUnit, Scheduler sc * * * @return an Observable that emits timestamped items from the source Observable - * @see RxJava wiki: timestamp + * @see ReactiveX operators documentation: Timestamp */ public final Observable> timestamp() { return timestamp(Schedulers.immediate()); @@ -8324,7 +8316,7 @@ public final Observable> timestamp() { * the {@link Scheduler} to use as a time source * @return an Observable that emits timestamped items from the source Observable with timestamps provided by * the {@code scheduler} - * @see RxJava wiki: timestamp + * @see ReactiveX operators documentation: Timestamp */ public final Observable> timestamp(Scheduler scheduler) { return lift(new OperatorTimestamp(scheduler)); @@ -8338,7 +8330,7 @@ public final Observable> timestamp(Scheduler scheduler) { * * * @return a {@code BlockingObservable} version of this Observable - * @see RxJava wiki: Blocking Observable Operators + * @see ReactiveX operators documentation: To */ public final BlockingObservable toBlocking() { return BlockingObservable.from(this); @@ -8367,7 +8359,7 @@ public final BlockingObservable toBlocking() { * * @return an Observable that emits a single item: a List containing all of the items emitted by the source * Observable - * @see RxJava wiki: toList + * @see ReactiveX operators documentation: To */ public final Observable> toList() { return lift(new OperatorToObservableList()); @@ -8391,7 +8383,7 @@ public final Observable> toList() { * the function that extracts the key from a source item to be used in the HashMap * @return an Observable that emits a single item: a HashMap containing the mapped items from the source * Observable - * @see RxJava wiki: toMap + * @see ReactiveX operators documentation: To */ public final Observable> toMap(Func1 keySelector) { return lift(new OperatorToMap(keySelector, UtilityFunctions.identity())); @@ -8418,7 +8410,7 @@ public final Observable> toMap(Func1 keySe * the function that extracts the value from a source item to be used in the HashMap * @return an Observable that emits a single item: a HashMap containing the mapped items from the source * Observable - * @see RxJava wiki: toMap + * @see ReactiveX operators documentation: To */ public final Observable> toMap(Func1 keySelector, Func1 valueSelector) { return lift(new OperatorToMap(keySelector, valueSelector)); @@ -8444,7 +8436,7 @@ public final Observable> toMap(Func1 ke * the function that returns a Map instance to be used * @return an Observable that emits a single item: a Map that contains the mapped items emitted by the * source Observable - * @see RxJava wiki: toMap + * @see ReactiveX operators documentation: To */ public final Observable> toMap(Func1 keySelector, Func1 valueSelector, Func0> mapFactory) { return lift(new OperatorToMap(keySelector, valueSelector, mapFactory)); @@ -8466,7 +8458,7 @@ public final Observable> toMap(Func1 ke * the function that extracts the key from the source items to be used as key in the HashMap * @return an Observable that emits a single item: a HashMap that contains an ArrayList of items mapped from * the source Observable - * @see RxJava wiki: toMap + * @see ReactiveX operators documentation: To */ public final Observable>> toMultimap(Func1 keySelector) { return lift(new OperatorToMultimap(keySelector, UtilityFunctions.identity())); @@ -8491,7 +8483,7 @@ public final Observable>> toMultimap(Func1RxJava wiki: toMap + * @see ReactiveX operators documentation: To */ public final Observable>> toMultimap(Func1 keySelector, Func1 valueSelector) { return lift(new OperatorToMultimap(keySelector, valueSelector)); @@ -8518,7 +8510,7 @@ public final Observable>> toMultimap(Func1RxJava wiki: toMap + * @see ReactiveX operators documentation: To */ public final Observable>> toMultimap(Func1 keySelector, Func1 valueSelector, Func0>> mapFactory) { return lift(new OperatorToMultimap(keySelector, valueSelector, mapFactory)); @@ -8547,7 +8539,7 @@ public final Observable>> toMultimap(Func1RxJava wiki: toMap + * @see ReactiveX operators documentation: To */ public final Observable>> toMultimap(Func1 keySelector, Func1 valueSelector, Func0>> mapFactory, Func1> collectionFactory) { return lift(new OperatorToMultimap(keySelector, valueSelector, mapFactory, collectionFactory)); @@ -8571,7 +8563,7 @@ public final Observable>> toMultimap(Func1RxJava wiki: toSortedList + * @see ReactiveX operators documentation: To */ public final Observable> toSortedList() { return lift(new OperatorToObservableSortedList()); @@ -8594,7 +8586,7 @@ public final Observable> toSortedList() { * that indicates their sort order * @return an Observable that emits a list that contains the items emitted by the source Observable in * sorted order - * @see RxJava wiki: toSortedList + * @see ReactiveX operators documentation: To */ public final Observable> toSortedList(Func2 sortFunction) { return lift(new OperatorToObservableSortedList(sortFunction)); @@ -8612,6 +8604,7 @@ public final Observable> toSortedList(Func2ReactiveX operators documentation: SubscribeOn */ public final Observable unsubscribeOn(Scheduler scheduler) { return lift(new OperatorUnsubscribeOn(scheduler)); @@ -8637,7 +8630,7 @@ public final Observable unsubscribeOn(Scheduler scheduler) { * a new one. * @return an Observable that emits connected, non-overlapping windows of items from the source Observable * whenever {@code closingSelector} emits an item - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(Func0> closingSelector) { return lift(new OperatorWindowWithObservable(closingSelector)); @@ -8661,7 +8654,7 @@ public final Observable> window(Func0RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(int count) { return window(count, count); @@ -8688,7 +8681,7 @@ 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 - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(int count, int skip) { return lift(new OperatorWindowWithSize(count, skip)); @@ -8716,7 +8709,7 @@ public final Observable> window(int count, int skip) { * @param unit * the unit of time that applies to the {@code timespan} and {@code timeshift} arguments * @return an Observable that emits new windows periodically as a fixed timespan elapses - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(long timespan, long timeshift, TimeUnit unit) { return window(timespan, timeshift, unit, Integer.MAX_VALUE, Schedulers.computation()); @@ -8746,7 +8739,7 @@ public final Observable> window(long timespan, long timeshift, Tim * @param scheduler * the {@link Scheduler} to use when determining the end and start of a window * @return an Observable that emits new windows periodically as a fixed timespan elapses - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(long timespan, long timeshift, TimeUnit unit, Scheduler scheduler) { return window(timespan, timeshift, unit, Integer.MAX_VALUE, scheduler); @@ -8779,7 +8772,7 @@ public final Observable> window(long timespan, long timeshift, Tim * @param scheduler * the {@link Scheduler} to use when determining the end and start of a window * @return an Observable that emits new windows periodically as a fixed timespan elapses - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(long timespan, long timeshift, TimeUnit unit, int count, Scheduler scheduler) { return lift(new OperatorWindowWithTime(timespan, timeshift, unit, count, scheduler)); @@ -8806,7 +8799,7 @@ public final Observable> window(long timespan, long timeshift, Tim * 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 * source Observable during fixed, consecutive durations - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(long timespan, TimeUnit unit) { return window(timespan, timespan, unit, Schedulers.computation()); @@ -8837,7 +8830,7 @@ public final Observable> window(long timespan, TimeUnit unit) { * @return an Observable that emits connected, non-overlapping windows of items from the source Observable * that were emitted during a fixed duration of time or when the window has reached maximum capacity * (whichever occurs first) - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(long timespan, TimeUnit unit, int count) { return window(timespan, unit, count, Schedulers.computation()); @@ -8870,7 +8863,7 @@ public final Observable> window(long timespan, TimeUnit unit, int * @return an Observable that emits connected, non-overlapping windows of items from the source Observable * that were emitted during a fixed duration of time or when the window has reached maximum capacity * (whichever occurs first) - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(long timespan, TimeUnit unit, int count, Scheduler scheduler) { return window(timespan, timespan, unit, count, scheduler); @@ -8899,7 +8892,7 @@ public final Observable> window(long timespan, TimeUnit unit, int * the {@link Scheduler} to use when determining the end and start of a window * @return an Observable that emits connected, non-overlapping windows containing items emitted by the * source Observable within a fixed duration - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(long timespan, TimeUnit unit, Scheduler scheduler) { return window(timespan, unit, Integer.MAX_VALUE, scheduler); @@ -8926,7 +8919,7 @@ public final Observable> window(long timespan, TimeUnit unit, Sche * emits an item, the associated window is closed and emitted * @return an Observable that emits windows of items emitted by the source Observable that are governed by * the specified window-governing Observables - * @see RxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(Observable windowOpenings, Func1> closingSelector) { return lift(new OperatorWindowWithStartEndObservable(windowOpenings, closingSelector)); @@ -8953,7 +8946,7 @@ public final Observable> window(ObservableRxJava wiki: window + * @see ReactiveX operators documentation: Window */ public final Observable> window(Observable boundary) { return lift(new OperatorWindowWithObservable(boundary)); @@ -8983,7 +8976,7 @@ public final Observable> window(Observable boundary) { * the items to be emitted by the resulting Observable * @return an Observable that pairs up values from the source Observable and the {@code other} Iterable * sequence and emits the results of {@code zipFunction} applied to these pairs - * @see RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final Observable zipWith(Iterable other, Func2 zipFunction) { return lift(new OperatorZipIterable(other, zipFunction)); @@ -9010,7 +9003,7 @@ public final Observable zipWith(Iterable other, Func2RxJava wiki: zip + * @see ReactiveX operators documentation: Zip */ public final Observable zipWith(Observable other, Func2 zipFunction) { return zip(this, other, zipFunction); diff --git a/src/main/java/rx/Observer.java b/src/main/java/rx/Observer.java index 38b08324d1..22f4de9704 100644 --- a/src/main/java/rx/Observer.java +++ b/src/main/java/rx/Observer.java @@ -23,7 +23,7 @@ * {@code Observable} will call an Observer's {@link #onCompleted} method exactly once or the Observer's * {@link #onError} method exactly once. * - * @see RxJava Wiki: Observable + * @see ReactiveX documentation: Observable * @param * the type of item the Observer expects to observe */ diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 021eb5807e..43f6b6d68d 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -26,7 +26,7 @@ * {@link Observable} will call a Subscriber's {@link #onCompleted} method exactly once or the Subscriber's * {@link #onError} method exactly once. * - * @see RxJava Wiki: Observable + * @see ReactiveX documentation: Observable * @param * the type of items the Subscriber expects to observe */ diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 6bffe27d09..851ce3c315 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -88,7 +88,7 @@ public static BlockingObservable from(final Observable o) { * the {@link Action1} to invoke for each item emitted by the {@code BlockingObservable} * @throws RuntimeException * if an error occurs - * @see RxJava wiki: forEach() + * @see ReactiveX documentation: Subscribe */ public void forEach(final Action1 onNext) { final CountDownLatch latch = new CountDownLatch(1); @@ -149,7 +149,7 @@ public void onNext(T args) { * * * @return an {@link Iterator} that can iterate over the items emitted by this {@code BlockingObservable} - * @see RxJava wiki: getIterator() + * @see ReactiveX documentation: To */ public Iterator getIterator() { return BlockingOperatorToIterator.toIterator(o); @@ -162,7 +162,7 @@ public Iterator getIterator() { * @return the first item emitted by this {@code BlockingObservable} * @throws NoSuchElementException * if this {@code BlockingObservable} emits no items - * @see RxJava wiki: first() + * @see ReactiveX documentation: First */ public T first() { return blockForSingle(o.first()); @@ -177,7 +177,7 @@ public T first() { * @return the first item emitted by this {@code BlockingObservable} that matches the predicate * @throws NoSuchElementException * if this {@code BlockingObservable} emits no such items - * @see RxJava wiki: first() + * @see ReactiveX documentation: First */ public T first(Func1 predicate) { return blockForSingle(o.first(predicate)); @@ -191,7 +191,7 @@ public T first(Func1 predicate) { * a default value to return if this {@code BlockingObservable} emits no items * @return the first item emitted by this {@code BlockingObservable}, or the default value if it emits no * items - * @see RxJava wiki: firstOrDefault() + * @see ReactiveX documentation: First */ public T firstOrDefault(T defaultValue) { return blockForSingle(o.map(UtilityFunctions.identity()).firstOrDefault(defaultValue)); @@ -207,7 +207,7 @@ public T firstOrDefault(T defaultValue) { * a predicate function to evaluate items emitted by this {@code BlockingObservable} * @return the first item emitted by this {@code BlockingObservable} that matches the predicate, or the * default value if this {@code BlockingObservable} emits no matching items - * @see RxJava wiki: firstOrDefault() + * @see ReactiveX documentation: First */ public T firstOrDefault(T defaultValue, Func1 predicate) { return blockForSingle(o.filter(predicate).map(UtilityFunctions.identity()).firstOrDefault(defaultValue)); @@ -222,7 +222,7 @@ public T firstOrDefault(T defaultValue, Func1 predicate) { * @return the last item emitted by this {@code BlockingObservable} * @throws NoSuchElementException * if this {@code BlockingObservable} emits no items - * @see RxJava wiki: last() + * @see ReactiveX documentation: Last */ public T last() { return blockForSingle(o.last()); @@ -239,7 +239,7 @@ public T last() { * @return the last item emitted by the {@code BlockingObservable} that matches the predicate * @throws NoSuchElementException * if this {@code BlockingObservable} emits no items - * @see RxJava wiki: last() + * @see ReactiveX documentation: Last */ public T last(final Func1 predicate) { return blockForSingle(o.last(predicate)); @@ -255,7 +255,7 @@ public T last(final Func1 predicate) { * a default value to return if this {@code BlockingObservable} emits no items * @return the last item emitted by the {@code BlockingObservable}, or the default value if it emits no * items - * @see RxJava wiki: lastOrDefault() + * @see ReactiveX documentation: Last */ public T lastOrDefault(T defaultValue) { return blockForSingle(o.map(UtilityFunctions.identity()).lastOrDefault(defaultValue)); @@ -273,7 +273,7 @@ public T lastOrDefault(T defaultValue) { * a predicate function to evaluate items emitted by this {@code BlockingObservable} * @return the last item emitted by this {@code BlockingObservable} that matches the predicate, or the * default value if it emits no matching items - * @see RxJava wiki: lastOrDefault() + * @see ReactiveX documentation: Last */ public T lastOrDefault(T defaultValue, Func1 predicate) { return blockForSingle(o.filter(predicate).map(UtilityFunctions.identity()).lastOrDefault(defaultValue)); @@ -290,7 +290,7 @@ public T lastOrDefault(T defaultValue, Func1 predicate) { * {@code BlockingObservable} has not yet emitted an item * @return an {@link Iterable} that on each iteration returns the item that this {@code BlockingObservable} * has most recently emitted - * @see RxJava wiki: mostRecent() + * @see ReactiveX documentation: First */ public Iterable mostRecent(T initialValue) { return BlockingOperatorMostRecent.mostRecent(o, initialValue); @@ -304,7 +304,7 @@ public Iterable mostRecent(T initialValue) { * * @return an {@link Iterable} that blocks upon each iteration until this {@code BlockingObservable} emits * a new item, whereupon the Iterable returns that item - * @see RxJava wiki: next() + * @see ReactiveX documentation: TakeLast */ public Iterable next() { return BlockingOperatorNext.next(o); @@ -321,7 +321,7 @@ public Iterable next() { * event. * * @return an Iterable that always returns the latest item emitted by this {@code BlockingObservable} - * @see RxJava wiki: latest() + * @see ReactiveX documentation: First */ public Iterable latest() { return BlockingOperatorLatest.latest(o); @@ -334,7 +334,7 @@ public Iterable latest() { * * * @return the single item emitted by this {@code BlockingObservable} - * @see RxJava wiki: single() + * @see ReactiveX documentation: First */ public T single() { return blockForSingle(o.single()); @@ -349,7 +349,7 @@ public T single() { * @param predicate * a predicate function to evaluate items emitted by this {@link BlockingObservable} * @return the single item emitted by this {@code BlockingObservable} that matches the predicate - * @see RxJava wiki: single() + * @see ReactiveX documentation: First */ public T single(Func1 predicate) { return blockForSingle(o.single(predicate)); @@ -366,7 +366,7 @@ public T single(Func1 predicate) { * a default value to return if this {@code BlockingObservable} emits no items * @return the single item emitted by this {@code BlockingObservable}, or the default value if it emits no * items - * @see RxJava wiki: singleOrDefault() + * @see ReactiveX documentation: First */ public T singleOrDefault(T defaultValue) { return blockForSingle(o.map(UtilityFunctions.identity()).singleOrDefault(defaultValue)); @@ -385,7 +385,7 @@ public T singleOrDefault(T defaultValue) { * a predicate function to evaluate items emitted by this {@code BlockingObservable} * @return the single item emitted by the {@code BlockingObservable} that matches the predicate, or the * default value if no such items are emitted - * @see RxJava wiki: singleOrDefault() + * @see ReactiveX documentation: First */ public T singleOrDefault(T defaultValue, Func1 predicate) { return blockForSingle(o.filter(predicate).map(UtilityFunctions.identity()).singleOrDefault(defaultValue)); @@ -403,7 +403,7 @@ public T singleOrDefault(T defaultValue, Func1 predicate) { * * * @return a {@link Future} that expects a single item to be emitted by this {@code BlockingObservable} - * @see RxJava wiki: toFuture() + * @see ReactiveX documentation: To */ public Future toFuture() { return BlockingOperatorToFuture.toFuture(o); @@ -415,7 +415,7 @@ public Future toFuture() { * * * @return an {@link Iterable} version of this {@code BlockingObservable} - * @see RxJava wiki: toIterable() + * @see ReactiveX documentation: To */ public Iterable toIterable() { return new Iterable() { diff --git a/src/main/java/rx/observables/ConnectableObservable.java b/src/main/java/rx/observables/ConnectableObservable.java index b95fab524d..a3c80ef1b4 100644 --- a/src/main/java/rx/observables/ConnectableObservable.java +++ b/src/main/java/rx/observables/ConnectableObservable.java @@ -47,6 +47,7 @@ protected ConnectableObservable(OnSubscribe onSubscribe) { * To disconnect from a synchronous source, use the {@link #connect(rx.functions.Action1)} method. * * @return the subscription representing the connection + * @see ReactiveX documentation: Connect */ public final Subscription connect() { final Subscription[] out = new Subscription[1]; @@ -65,6 +66,7 @@ public void call(Subscription t1) { * @param connection * the action that receives the connection subscription before the subscription to source happens * allowing the caller to synchronously disconnect a synchronous source + * @see ReactiveX documentation: Connect */ public abstract void connect(Action1 connection); @@ -73,6 +75,7 @@ public void call(Subscription t1) { * is at least one subscription to this {@code ConnectableObservable}. * * @return a {@link Observable} + * @see ReactiveX documentation: RefCount */ public Observable refCount() { return create(new OnSubscribeRefCount(this)); diff --git a/src/main/java/rx/observables/GroupedObservable.java b/src/main/java/rx/observables/GroupedObservable.java index 9c6d0e7027..8daea16643 100644 --- a/src/main/java/rx/observables/GroupedObservable.java +++ b/src/main/java/rx/observables/GroupedObservable.java @@ -33,7 +33,7 @@ * @param * the type of the items emitted by the {@code GroupedObservable} * @see Observable#groupBy(Func1) - * @see RxJava Wiki: groupBy() and groupByUntil() + * @see ReactiveX documentation: GroupBy */ public class GroupedObservable extends Observable { private final K key; From 5154351f2c74645cd336da58af06ac81e55f0607 Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Mon, 12 Jan 2015 14:33:48 -0800 Subject: [PATCH 052/857] Fail early if a null subscription is added to a CompositeSubscription. Otherwise, it'll just fail late when unsubscribing, which is much harder to trace. --- src/main/java/rx/subscriptions/CompositeSubscription.java | 3 +++ .../java/rx/subscriptions/CompositeSubscriptionTest.java | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index fea7b70910..02f54da17c 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -55,6 +55,9 @@ public synchronized boolean isUnsubscribed() { * the {@link Subscription} to add */ public void add(final Subscription s) { + if (s == null) { + throw new IllegalArgumentException("Added Subscription cannot be null."); + } Subscription unsubscribe = null; synchronized (this) { if (unsubscribed) { diff --git a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java index 4cff2a3e6a..c69589cb31 100644 --- a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java +++ b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java @@ -337,4 +337,11 @@ public void testTryRemoveIfNotIn() { csub.remove(csub1); // try removing agian } + + @Test(expected = IllegalArgumentException.class) + public void testAddingNullSubscriptionIllegal() { + CompositeSubscription csub = new CompositeSubscription(); + csub.add(null); + } + } From ed63daa9520b06f574eb387a43c2c2620bc70134 Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Mon, 12 Jan 2015 14:58:49 -0800 Subject: [PATCH 053/857] Fixes indentation. --- src/test/java/rx/subscriptions/CompositeSubscriptionTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java index c69589cb31..ad95939b22 100644 --- a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java +++ b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java @@ -340,8 +340,8 @@ public void testTryRemoveIfNotIn() { @Test(expected = IllegalArgumentException.class) public void testAddingNullSubscriptionIllegal() { - CompositeSubscription csub = new CompositeSubscription(); - csub.add(null); + CompositeSubscription csub = new CompositeSubscription(); + csub.add(null); } } From 224cafb256ad1f0c34741cd1bb094bc4d9584142 Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Mon, 12 Jan 2015 18:24:41 -0800 Subject: [PATCH 054/857] IllegalArgumentException > NPE --- src/main/java/rx/subscriptions/CompositeSubscription.java | 2 +- src/test/java/rx/subscriptions/CompositeSubscriptionTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index 02f54da17c..d69a413554 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -56,7 +56,7 @@ public synchronized boolean isUnsubscribed() { */ public void add(final Subscription s) { if (s == null) { - throw new IllegalArgumentException("Added Subscription cannot be null."); + throw new NullPointerException("Added Subscription cannot be null."); } Subscription unsubscribe = null; synchronized (this) { diff --git a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java index ad95939b22..18ac45f198 100644 --- a/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java +++ b/src/test/java/rx/subscriptions/CompositeSubscriptionTest.java @@ -338,7 +338,7 @@ public void testTryRemoveIfNotIn() { csub.remove(csub1); // try removing agian } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NullPointerException.class) public void testAddingNullSubscriptionIllegal() { CompositeSubscription csub = new CompositeSubscription(); csub.add(null); From 74e54add53bfd3f90b439b6918f6b7fe742f090c Mon Sep 17 00:00:00 2001 From: Duncan Irvine Date: Thu, 15 Jan 2015 15:08:03 +1100 Subject: [PATCH 055/857] Fix for https://github.com/ReactiveX/RxJava/issues/2191 - OperatorMulticast fails to unsubscribe from source. --- .../internal/operators/OperatorMulticast.java | 2 +- .../operators/OperatorMulticastTest.java | 249 ++++++++++++++++++ 2 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/test/java/rx/internal/operators/OperatorMulticastTest.java diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java index 06f0733766..e294bfa8f4 100644 --- a/src/main/java/rx/internal/operators/OperatorMulticast.java +++ b/src/main/java/rx/internal/operators/OperatorMulticast.java @@ -143,7 +143,7 @@ public void call() { subscriptionIsNull = subscription == null; } if (!subscriptionIsNull) - source.unsafeSubscribe(subscription); + source.subscribe(subscription); } } } \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorMulticastTest.java b/src/test/java/rx/internal/operators/OperatorMulticastTest.java new file mode 100644 index 0000000000..b55efff539 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorMulticastTest.java @@ -0,0 +1,249 @@ +package rx.internal.operators; + +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import rx.Observable; +import rx.Scheduler; +import rx.Scheduler.Worker; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.observables.ConnectableObservable; + +public class OperatorMulticastTest { + + /** + * test the basic expectation of OperatorMulticast via replay + */ + @Test + public void testIssue2191_UnsubscribeSource() { + // setup mocks + Action1 sourceNext = mock(Action1.class); + Action0 sourceCompleted = mock(Action0.class); + Action0 sourceUnsubscribed = mock(Action0.class); + Subscriber spiedSubscriberBeforeConnect = subscriberSpy(); + Subscriber spiedSubscriberAfterConnect = subscriberSpy(); + + // Observable under test + Observable source = Observable.just(1,2); + + ConnectableObservable replay = source + .doOnNext(sourceNext) + .doOnUnsubscribe(sourceUnsubscribed) + .doOnCompleted(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + + // verify interactions + verify(sourceNext, times(1)).call(1); + verify(sourceNext, times(1)).call(2); + verify(sourceCompleted, times(1)).call(); + verifySubscriberSpy(spiedSubscriberBeforeConnect, 2, 4); + verifySubscriberSpy(spiedSubscriberAfterConnect, 2, 4); + + verify(sourceUnsubscribed, times(1)).call(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn + * + * @throws Exception + */ + @Test + public void testIssue2191_SchedulerUnsubscribe() throws Exception { + // setup mocks + Action1 sourceNext = mock(Action1.class); + Action0 sourceCompleted = mock(Action0.class); + Action0 sourceUnsubscribed = mock(Action0.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Subscription mockSubscription = mock(Subscription.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber spiedSubscriberBeforeConnect = subscriberSpy(); + Subscriber spiedSubscriberAfterConnect = subscriberSpy(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Observable under test + ConnectableObservable replay = Observable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnUnsubscribe(sourceUnsubscribed) + .doOnCompleted(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + // verify interactions + verify(sourceNext, times(1)).call(1); + verify(sourceNext, times(1)).call(2); + verify(sourceNext, times(1)).call(3); + verify(sourceCompleted, times(1)).call(); + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Action0)notNull()); + verifySubscriberSpy(spiedSubscriberBeforeConnect, 2, 6); + verifySubscriberSpy(spiedSubscriberAfterConnect, 2, 6); + + verify(spiedWorker, times(1)).unsubscribe(); + verify(sourceUnsubscribed, times(1)).call(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn + * + * @throws Exception + */ + @Test + public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { + // setup mocks + Action1 sourceNext = mock(Action1.class); + Action0 sourceCompleted = mock(Action0.class); + Action1 sourceError = mock(Action1.class); + Action0 sourceUnsubscribed = mock(Action0.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Subscription mockSubscription = mock(Subscription.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber spiedSubscriberBeforeConnect = subscriberSpy(); + Subscriber spiedSubscriberAfterConnect = subscriberSpy(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Observable under test + Func1 mockFunc = mock(Func1.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.call(1)).thenReturn(1); + when(mockFunc.call(2)).thenThrow(illegalArgumentException); + ConnectableObservable replay = Observable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnUnsubscribe(sourceUnsubscribed) + .doOnCompleted(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + // verify interactions + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Action0)notNull()); + verify(sourceNext, times(1)).call(1); + verify(sourceError, times(1)).call(illegalArgumentException); + verifySubscriberSpy(spiedSubscriberBeforeConnect, 2, 2, illegalArgumentException); + verifySubscriberSpy(spiedSubscriberAfterConnect, 2, 2, illegalArgumentException); + + verify(spiedWorker, times(1)).unsubscribe(); + verify(sourceUnsubscribed, times(1)).call(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + } + + public static Subscriber subscriberSpy() { + return spy(new EmptySubscriber()); + } + + private void verifySubscriberSpy(Subscriber spiedSubscriber, int numSubscriptions, int numItemsExpected) { + verify(spiedSubscriber, times(numSubscriptions)).onStart(); + verify(spiedSubscriber, times(numItemsExpected)).onNext(notNull()); + verify(spiedSubscriber, times(numSubscriptions)).onCompleted(); + verifyNoMoreInteractions(spiedSubscriber); + } + private void verifySubscriberSpy(Subscriber spiedSubscriber, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(spiedSubscriber, times(numSubscriptions)).onStart(); + verify(spiedSubscriber, times(numItemsExpected)).onNext(notNull()); + verify(spiedSubscriber, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(spiedSubscriber); + } + + public static Worker workerSpy(final Subscription mockSubscription) { + return spy(new InprocessWorker(mockSubscription)); + } + + private static class EmptySubscriber extends Subscriber { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Object o) { + + } + } + + private static class InprocessWorker extends Worker { + private final Subscription mockSubscription; + public boolean unsubscribed; + + public InprocessWorker(Subscription mockSubscription) { + this.mockSubscription = mockSubscription; + } + + @Override + public Subscription schedule(Action0 action) { + action.call(); + return mockSubscription; // this subscription is returned but discarded + } + + @Override + public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + action.call(); + return mockSubscription; + } + + @Override + public void unsubscribe() { + unsubscribed = true; + } + + @Override + public boolean isUnsubscribed() { + return unsubscribed; + } + } +} \ No newline at end of file From ce76ea9b8e3b27d0e3c34149963deff9dd99e781 Mon Sep 17 00:00:00 2001 From: Duncan Irvine Date: Thu, 15 Jan 2015 18:38:53 +1100 Subject: [PATCH 056/857] Moved test cases for issue #2191 into OperatorReplayTest --- .../operators/OperatorMulticastTest.java | 249 ------------------ .../operators/OperatorReplayTest.java | 221 +++++++++++++++- 2 files changed, 216 insertions(+), 254 deletions(-) delete mode 100644 src/test/java/rx/internal/operators/OperatorMulticastTest.java diff --git a/src/test/java/rx/internal/operators/OperatorMulticastTest.java b/src/test/java/rx/internal/operators/OperatorMulticastTest.java deleted file mode 100644 index b55efff539..0000000000 --- a/src/test/java/rx/internal/operators/OperatorMulticastTest.java +++ /dev/null @@ -1,249 +0,0 @@ -package rx.internal.operators; - -import static org.mockito.Mockito.*; - -import java.util.concurrent.TimeUnit; - -import org.junit.Test; - -import rx.Observable; -import rx.Scheduler; -import rx.Scheduler.Worker; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.observables.ConnectableObservable; - -public class OperatorMulticastTest { - - /** - * test the basic expectation of OperatorMulticast via replay - */ - @Test - public void testIssue2191_UnsubscribeSource() { - // setup mocks - Action1 sourceNext = mock(Action1.class); - Action0 sourceCompleted = mock(Action0.class); - Action0 sourceUnsubscribed = mock(Action0.class); - Subscriber spiedSubscriberBeforeConnect = subscriberSpy(); - Subscriber spiedSubscriberAfterConnect = subscriberSpy(); - - // Observable under test - Observable source = Observable.just(1,2); - - ConnectableObservable replay = source - .doOnNext(sourceNext) - .doOnUnsubscribe(sourceUnsubscribed) - .doOnCompleted(sourceCompleted) - .replay(); - - replay.subscribe(spiedSubscriberBeforeConnect); - replay.subscribe(spiedSubscriberBeforeConnect); - replay.connect(); - replay.subscribe(spiedSubscriberAfterConnect); - replay.subscribe(spiedSubscriberAfterConnect); - - - // verify interactions - verify(sourceNext, times(1)).call(1); - verify(sourceNext, times(1)).call(2); - verify(sourceCompleted, times(1)).call(); - verifySubscriberSpy(spiedSubscriberBeforeConnect, 2, 4); - verifySubscriberSpy(spiedSubscriberAfterConnect, 2, 4); - - verify(sourceUnsubscribed, times(1)).call(); - - verifyNoMoreInteractions(sourceNext); - verifyNoMoreInteractions(sourceCompleted); - verifyNoMoreInteractions(sourceUnsubscribed); - verifyNoMoreInteractions(spiedSubscriberBeforeConnect); - verifyNoMoreInteractions(spiedSubscriberAfterConnect); - - } - - /** - * Specifically test interaction with a Scheduler with subscribeOn - * - * @throws Exception - */ - @Test - public void testIssue2191_SchedulerUnsubscribe() throws Exception { - // setup mocks - Action1 sourceNext = mock(Action1.class); - Action0 sourceCompleted = mock(Action0.class); - Action0 sourceUnsubscribed = mock(Action0.class); - final Scheduler mockScheduler = mock(Scheduler.class); - final Subscription mockSubscription = mock(Subscription.class); - Worker spiedWorker = workerSpy(mockSubscription); - Subscriber spiedSubscriberBeforeConnect = subscriberSpy(); - Subscriber spiedSubscriberAfterConnect = subscriberSpy(); - - when(mockScheduler.createWorker()).thenReturn(spiedWorker); - - // Observable under test - ConnectableObservable replay = Observable.just(1, 2, 3) - .doOnNext(sourceNext) - .doOnUnsubscribe(sourceUnsubscribed) - .doOnCompleted(sourceCompleted) - .subscribeOn(mockScheduler).replay(); - - replay.subscribe(spiedSubscriberBeforeConnect); - replay.subscribe(spiedSubscriberBeforeConnect); - replay.connect(); - replay.subscribe(spiedSubscriberAfterConnect); - replay.subscribe(spiedSubscriberAfterConnect); - - // verify interactions - verify(sourceNext, times(1)).call(1); - verify(sourceNext, times(1)).call(2); - verify(sourceNext, times(1)).call(3); - verify(sourceCompleted, times(1)).call(); - verify(mockScheduler, times(1)).createWorker(); - verify(spiedWorker, times(1)).schedule((Action0)notNull()); - verifySubscriberSpy(spiedSubscriberBeforeConnect, 2, 6); - verifySubscriberSpy(spiedSubscriberAfterConnect, 2, 6); - - verify(spiedWorker, times(1)).unsubscribe(); - verify(sourceUnsubscribed, times(1)).call(); - - verifyNoMoreInteractions(sourceNext); - verifyNoMoreInteractions(sourceCompleted); - verifyNoMoreInteractions(sourceUnsubscribed); - verifyNoMoreInteractions(spiedWorker); - verifyNoMoreInteractions(mockSubscription); - verifyNoMoreInteractions(mockScheduler); - verifyNoMoreInteractions(spiedSubscriberBeforeConnect); - verifyNoMoreInteractions(spiedSubscriberAfterConnect); - } - - /** - * Specifically test interaction with a Scheduler with subscribeOn - * - * @throws Exception - */ - @Test - public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { - // setup mocks - Action1 sourceNext = mock(Action1.class); - Action0 sourceCompleted = mock(Action0.class); - Action1 sourceError = mock(Action1.class); - Action0 sourceUnsubscribed = mock(Action0.class); - final Scheduler mockScheduler = mock(Scheduler.class); - final Subscription mockSubscription = mock(Subscription.class); - Worker spiedWorker = workerSpy(mockSubscription); - Subscriber spiedSubscriberBeforeConnect = subscriberSpy(); - Subscriber spiedSubscriberAfterConnect = subscriberSpy(); - - when(mockScheduler.createWorker()).thenReturn(spiedWorker); - - // Observable under test - Func1 mockFunc = mock(Func1.class); - IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); - when(mockFunc.call(1)).thenReturn(1); - when(mockFunc.call(2)).thenThrow(illegalArgumentException); - ConnectableObservable replay = Observable.just(1, 2, 3).map(mockFunc) - .doOnNext(sourceNext) - .doOnUnsubscribe(sourceUnsubscribed) - .doOnCompleted(sourceCompleted) - .doOnError(sourceError) - .subscribeOn(mockScheduler).replay(); - - replay.subscribe(spiedSubscriberBeforeConnect); - replay.subscribe(spiedSubscriberBeforeConnect); - replay.connect(); - replay.subscribe(spiedSubscriberAfterConnect); - replay.subscribe(spiedSubscriberAfterConnect); - - // verify interactions - verify(mockScheduler, times(1)).createWorker(); - verify(spiedWorker, times(1)).schedule((Action0)notNull()); - verify(sourceNext, times(1)).call(1); - verify(sourceError, times(1)).call(illegalArgumentException); - verifySubscriberSpy(spiedSubscriberBeforeConnect, 2, 2, illegalArgumentException); - verifySubscriberSpy(spiedSubscriberAfterConnect, 2, 2, illegalArgumentException); - - verify(spiedWorker, times(1)).unsubscribe(); - verify(sourceUnsubscribed, times(1)).call(); - - verifyNoMoreInteractions(sourceNext); - verifyNoMoreInteractions(sourceCompleted); - verifyNoMoreInteractions(sourceError); - verifyNoMoreInteractions(sourceUnsubscribed); - verifyNoMoreInteractions(spiedWorker); - verifyNoMoreInteractions(mockSubscription); - verifyNoMoreInteractions(mockScheduler); - verifyNoMoreInteractions(spiedSubscriberBeforeConnect); - verifyNoMoreInteractions(spiedSubscriberAfterConnect); - } - - public static Subscriber subscriberSpy() { - return spy(new EmptySubscriber()); - } - - private void verifySubscriberSpy(Subscriber spiedSubscriber, int numSubscriptions, int numItemsExpected) { - verify(spiedSubscriber, times(numSubscriptions)).onStart(); - verify(spiedSubscriber, times(numItemsExpected)).onNext(notNull()); - verify(spiedSubscriber, times(numSubscriptions)).onCompleted(); - verifyNoMoreInteractions(spiedSubscriber); - } - private void verifySubscriberSpy(Subscriber spiedSubscriber, int numSubscriptions, int numItemsExpected, Throwable error) { - verify(spiedSubscriber, times(numSubscriptions)).onStart(); - verify(spiedSubscriber, times(numItemsExpected)).onNext(notNull()); - verify(spiedSubscriber, times(numSubscriptions)).onError(error); - verifyNoMoreInteractions(spiedSubscriber); - } - - public static Worker workerSpy(final Subscription mockSubscription) { - return spy(new InprocessWorker(mockSubscription)); - } - - private static class EmptySubscriber extends Subscriber { - @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onNext(Object o) { - - } - } - - private static class InprocessWorker extends Worker { - private final Subscription mockSubscription; - public boolean unsubscribed; - - public InprocessWorker(Subscription mockSubscription) { - this.mockSubscription = mockSubscription; - } - - @Override - public Subscription schedule(Action0 action) { - action.call(); - return mockSubscription; // this subscription is returned but discarded - } - - @Override - public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { - action.call(); - return mockSubscription; - } - - @Override - public void unsubscribe() { - unsubscribed = true; - } - - @Override - public boolean isUnsubscribed() { - return unsubscribed; - } - } -} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 7820aae380..6a6e48f068 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -16,21 +16,22 @@ package rx.internal.operators; 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.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.notNull; +import static org.mockito.Mockito.*; + import org.junit.Test; import org.mockito.InOrder; import rx.Observable; import rx.Observer; +import rx.Scheduler; +import rx.Scheduler.Worker; +import rx.Subscription; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; @@ -465,4 +466,214 @@ public void call() { assertEquals(2, effectCounter.get()); } } + + + /** + * test the basic expectation of OperatorMulticast via replay + */ + @Test + public void testIssue2191_UnsubscribeSource() { + // setup mocks + Action1 sourceNext = mock(Action1.class); + Action0 sourceCompleted = mock(Action0.class); + Action0 sourceUnsubscribed = mock(Action0.class); + Observer spiedSubscriberBeforeConnect = mock(Observer.class); + Observer spiedSubscriberAfterConnect = mock(Observer.class); + + // Observable under test + Observable source = Observable.just(1,2); + + ConnectableObservable replay = source + .doOnNext(sourceNext) + .doOnUnsubscribe(sourceUnsubscribed) + .doOnCompleted(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + + // verify interactions + verify(sourceNext, times(1)).call(1); + verify(sourceNext, times(1)).call(2); + verify(sourceCompleted, times(1)).call(); + verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4); + verifyObserverMock(spiedSubscriberAfterConnect, 2, 4); + + verify(sourceUnsubscribed, times(1)).call(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn + * + * @throws Exception + */ + @Test + public void testIssue2191_SchedulerUnsubscribe() throws Exception { + // setup mocks + Action1 sourceNext = mock(Action1.class); + Action0 sourceCompleted = mock(Action0.class); + Action0 sourceUnsubscribed = mock(Action0.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Subscription mockSubscription = mock(Subscription.class); + Worker spiedWorker = workerSpy(mockSubscription); + Observer mockObserverBeforeConnect = mock(Observer.class); + Observer mockObserverAfterConnect = mock(Observer.class); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Observable under test + ConnectableObservable replay = Observable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnUnsubscribe(sourceUnsubscribed) + .doOnCompleted(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + // verify interactions + verify(sourceNext, times(1)).call(1); + verify(sourceNext, times(1)).call(2); + verify(sourceNext, times(1)).call(3); + verify(sourceCompleted, times(1)).call(); + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Action0)notNull()); + verifyObserverMock(mockObserverBeforeConnect, 2, 6); + verifyObserverMock(mockObserverAfterConnect, 2, 6); + + verify(spiedWorker, times(1)).unsubscribe(); + verify(sourceUnsubscribed, times(1)).call(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn + * + * @throws Exception + */ + @Test + public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { + // setup mocks + Action1 sourceNext = mock(Action1.class); + Action0 sourceCompleted = mock(Action0.class); + Action1 sourceError = mock(Action1.class); + Action0 sourceUnsubscribed = mock(Action0.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Subscription mockSubscription = mock(Subscription.class); + Worker spiedWorker = workerSpy(mockSubscription); + Observer mockObserverBeforeConnect = mock(Observer.class); + Observer mockObserverAfterConnect = mock(Observer.class); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Observable under test + Func1 mockFunc = mock(Func1.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.call(1)).thenReturn(1); + when(mockFunc.call(2)).thenThrow(illegalArgumentException); + ConnectableObservable replay = Observable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnUnsubscribe(sourceUnsubscribed) + .doOnCompleted(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + // verify interactions + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Action0)notNull()); + verify(sourceNext, times(1)).call(1); + verify(sourceError, times(1)).call(illegalArgumentException); + verifyObserver(mockObserverBeforeConnect, 2, 2, illegalArgumentException); + verifyObserver(mockObserverAfterConnect, 2, 2, illegalArgumentException); + + verify(spiedWorker, times(1)).unsubscribe(); + verify(sourceUnsubscribed, times(1)).call(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + private static void verifyObserverMock(Observer mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext(notNull()); + verify(mock, times(numSubscriptions)).onCompleted(); + verifyNoMoreInteractions(mock); + } + + private static void verifyObserver(Observer mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext(notNull()); + verify(mock, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(mock); + } + + public static Worker workerSpy(final Subscription mockSubscription) { + return spy(new InprocessWorker(mockSubscription)); + } + + + private static class InprocessWorker extends Worker { + private final Subscription mockSubscription; + public boolean unsubscribed; + + public InprocessWorker(Subscription mockSubscription) { + this.mockSubscription = mockSubscription; + } + + @Override + public Subscription schedule(Action0 action) { + action.call(); + return mockSubscription; // this subscription is returned but discarded + } + + @Override + public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + action.call(); + return mockSubscription; + } + + @Override + public void unsubscribe() { + unsubscribed = true; + } + + @Override + public boolean isUnsubscribed() { + return unsubscribed; + } + } + } \ No newline at end of file From 939a3b1011a9c2964a62a61b4cf20aff90e33459 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 15 Jan 2015 15:42:55 +0100 Subject: [PATCH 057/857] Unbroken TestSubscriber --- src/main/java/rx/observers/TestSubscriber.java | 13 ++++++------- .../rx/internal/operators/OperatorRetryTest.java | 8 +++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 4131957405..ff9842f883 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -16,12 +16,9 @@ package rx.observers; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; -import rx.Notification; -import rx.Observer; -import rx.Subscriber; +import rx.*; /** * A {@code TestSubscriber} is a variety of {@link Subscriber} that you can use for unit testing, to perform @@ -227,9 +224,11 @@ public void awaitTerminalEvent() { * @throws RuntimeException * if the Subscriber is interrupted before the Observable is able to complete */ - public boolean awaitTerminalEvent(long timeout, TimeUnit unit) { + public void awaitTerminalEvent(long timeout, TimeUnit unit) { try { - return latch.await(timeout, unit); + if (!latch.await(timeout, unit)) { + throw new RuntimeException(new TimeoutException()); + } } catch (InterruptedException e) { throw new RuntimeException("Interrupted", e); } diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index 4fcafed32c..7783eadb2d 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -730,15 +730,12 @@ public void testRetryWithBackpressureParallel() throws InterruptedException { exec.execute(new Runnable() { @Override public void run() { + final AtomicInteger nexts = new AtomicInteger(); try { - final AtomicInteger nexts = new AtomicInteger(); Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); TestSubscriber ts = new TestSubscriber(); origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); - if (!ts.awaitTerminalEvent(2, TimeUnit.SECONDS)) { - timeouts.incrementAndGet(); - System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); - } + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); if (ts.getOnNextEvents().size() != NUM_RETRIES + 2) { data.incrementAndGet(); } @@ -750,6 +747,7 @@ public void run() { } } catch (Throwable t) { timeouts.incrementAndGet(); + System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); } cdl.countDown(); } From b5aaf82ea8c7addd2cd197f1c61ddec9ee5ee962 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 19 Jan 2015 09:10:53 +0100 Subject: [PATCH 058/857] ScheduledExecutorService: call purge periodically on JDK 6 to avoid cancelled task-retention. --- .../internal/schedulers/NewThreadWorker.java | 118 ++++++++++++++++-- .../rx/schedulers/CachedThreadScheduler.java | 1 + .../GenericScheduledExecutorService.java | 14 ++- .../schedulers/CachedThreadSchedulerTest.java | 91 +++++++------- 4 files changed, 158 insertions(+), 66 deletions(-) diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index ea80c651c3..65b8ac92a9 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -16,10 +16,14 @@ package rx.internal.schedulers; import java.lang.reflect.Method; +import java.util.Iterator; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; import rx.*; +import rx.exceptions.Exceptions; import rx.functions.Action0; +import rx.internal.util.RxThreadFactory; import rx.plugins.*; import rx.subscriptions.Subscriptions; @@ -30,24 +34,111 @@ public class NewThreadWorker extends Scheduler.Worker implements Subscription { private final ScheduledExecutorService executor; private final RxJavaSchedulersHook schedulersHook; volatile boolean isUnsubscribed; - + /** The purge frequency in milliseconds. */ + private static final String FREQUENCY_KEY = "io.reactivex.rxjava.scheduler.jdk6.purge-frequency-millis"; + /** Force the use of purge (true/false). */ + private static final String PURGE_FORCE_KEY = "io.reactivex.rxjava.scheduler.jdk6.purge-force"; + private static final String PURGE_THREAD_PREFIX = "RxSchedulerPurge-"; + /** Forces the use of purge even if setRemoveOnCancelPolicy is available. */ + private static final boolean PURGE_FORCE; + /** The purge frequency in milliseconds. */ + public static final int PURGE_FREQUENCY; + private static final ConcurrentHashMap EXECUTORS; + private static final AtomicReference PURGE; + static { + EXECUTORS = new ConcurrentHashMap(); + PURGE = new AtomicReference(); + PURGE_FORCE = Boolean.getBoolean(PURGE_FORCE_KEY); + PURGE_FREQUENCY = Integer.getInteger(FREQUENCY_KEY, 1000); + } + /** + * Registers the given executor service and starts the purge thread if not already started. + *

{@code public} visibility reason: called from other package(s) within RxJava + * @param service a scheduled thread pool executor instance + */ + public static void registerExecutor(ScheduledThreadPoolExecutor service) { + do { + ScheduledExecutorService exec = PURGE.get(); + if (exec != null) { + break; + } + exec = Executors.newScheduledThreadPool(1, new RxThreadFactory(PURGE_THREAD_PREFIX)); + if (PURGE.compareAndSet(null, exec)) { + exec.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + purgeExecutors(); + } + }, PURGE_FREQUENCY, PURGE_FREQUENCY, TimeUnit.MILLISECONDS); + + break; + } + } while (true); + + EXECUTORS.putIfAbsent(service, service); + } + /** + * Deregisters the executor service. + *

{@code public} visibility reason: called from other package(s) within RxJava + * @param service a scheduled thread pool executor instance + */ + public static void deregisterExecutor(ScheduledExecutorService service) { + EXECUTORS.remove(service); + } + /** Purges each registered executor and eagerly evicts shutdown executors. */ + static void purgeExecutors() { + try { + Iterator it = EXECUTORS.keySet().iterator(); + while (it.hasNext()) { + ScheduledThreadPoolExecutor exec = it.next(); + if (!exec.isShutdown()) { + exec.purge(); + } else { + it.remove(); + } + } + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + RxJavaPlugins.getInstance().getErrorHandler().handleError(t); + } + } + + /** + * Tries to enable the Java 7+ setRemoveOnCancelPolicy. + *

{@code public} visibility reason: called from other package(s) within RxJava. + * If the method returns false, the {@link #registerExecutor(ScheduledThreadPoolExecutor)} may + * be called to enable the backup option of purging the executors. + * @param exec the executor to call setRemoveOnCaneclPolicy if available. + * @return true if the policy was successfully enabled + */ + public static boolean tryEnableCancelPolicy(ScheduledExecutorService exec) { + if (!PURGE_FORCE) { + for (Method m : exec.getClass().getMethods()) { + if (m.getName().equals("setRemoveOnCancelPolicy") + && m.getParameterTypes().length == 1 + && m.getParameterTypes()[0] == Boolean.TYPE) { + try { + m.invoke(exec, true); + return true; + } catch (Exception ex) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); + } + } + } + } + return false; + } + /* package */ public NewThreadWorker(ThreadFactory threadFactory) { - executor = Executors.newScheduledThreadPool(1, threadFactory); + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1, threadFactory); // Java 7+: cancelled future tasks can be removed from the executor thus avoiding memory leak - for (Method m : executor.getClass().getMethods()) { - if (m.getName().equals("setRemoveOnCancelPolicy") - && m.getParameterTypes().length == 1 - && m.getParameterTypes()[0] == Boolean.TYPE) { - try { - m.invoke(executor, true); - } catch (Exception ex) { - RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); - } - break; - } + boolean cancelSupported = tryEnableCancelPolicy(exec); + if (!cancelSupported && exec instanceof ScheduledThreadPoolExecutor) { + registerExecutor((ScheduledThreadPoolExecutor)exec); } schedulersHook = RxJavaPlugins.getInstance().getSchedulersHook(); + executor = exec; } @Override @@ -88,6 +179,7 @@ public ScheduledAction scheduleActual(final Action0 action, long delayTime, Time public void unsubscribe() { isUnsubscribed = true; executor.shutdownNow(); + deregisterExecutor(executor); } @Override diff --git a/src/main/java/rx/schedulers/CachedThreadScheduler.java b/src/main/java/rx/schedulers/CachedThreadScheduler.java index fe70a58a43..f1cd815b64 100644 --- a/src/main/java/rx/schedulers/CachedThreadScheduler.java +++ b/src/main/java/rx/schedulers/CachedThreadScheduler.java @@ -110,6 +110,7 @@ public Worker createWorker() { private static final class EventLoopWorker extends Scheduler.Worker { private final CompositeSubscription innerSubscription = new CompositeSubscription(); private final ThreadWorker threadWorker; + @SuppressWarnings("unused") volatile int once; static final AtomicIntegerFieldUpdater ONCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(EventLoopWorker.class, "once"); diff --git a/src/main/java/rx/schedulers/GenericScheduledExecutorService.java b/src/main/java/rx/schedulers/GenericScheduledExecutorService.java index 2feccf92d6..ca133275e7 100644 --- a/src/main/java/rx/schedulers/GenericScheduledExecutorService.java +++ b/src/main/java/rx/schedulers/GenericScheduledExecutorService.java @@ -16,12 +16,10 @@ package rx.schedulers; import rx.Scheduler; +import rx.internal.schedulers.NewThreadWorker; import rx.internal.util.RxThreadFactory; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.*; /** * A default {@link ScheduledExecutorService} that can be used for scheduling actions when a {@link Scheduler} implementation doesn't have that ability. @@ -49,7 +47,13 @@ private GenericScheduledExecutorService() { if (count > 8) { count = 8; } - executor = Executors.newScheduledThreadPool(count, THREAD_FACTORY); + ScheduledExecutorService exec = Executors.newScheduledThreadPool(count, THREAD_FACTORY); + if (!NewThreadWorker.tryEnableCancelPolicy(exec)) { + if (exec instanceof ScheduledThreadPoolExecutor) { + NewThreadWorker.registerExecutor((ScheduledThreadPoolExecutor)exec); + } + } + executor = exec; } /** diff --git a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java b/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java index 4b6b02eff2..2b22e78068 100644 --- a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java +++ b/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java @@ -26,6 +26,7 @@ import rx.Observable; import rx.Scheduler; import rx.functions.*; +import rx.internal.schedulers.NewThreadWorker; import static org.junit.Assert.assertTrue; public class CachedThreadSchedulerTest extends AbstractSchedulerConcurrencyTests { @@ -73,55 +74,49 @@ public final void testHandledErrorIsNotDeliveredToThreadHandler() throws Interru @Test(timeout = 30000) public void testCancelledTaskRetention() throws InterruptedException { - try { - ScheduledThreadPoolExecutor.class.getMethod("setRemoveOnCancelPolicy", Boolean.TYPE); - - 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); - - Scheduler.Worker w = Schedulers.io().createWorker(); - for (int i = 0; i < 750000; i++) { - if (i % 50000 == 0) { - System.out.println(" -> still scheduling: " + i); - } - w.schedule(Actions.empty(), 1, TimeUnit.DAYS); + 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); + + Scheduler.Worker w = Schedulers.io().createWorker(); + for (int i = 0; i < 750000; i++) { + if (i % 50000 == 0) { + System.out.println(" -> still scheduling: " + i); } - - 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(1000); - - 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) { - Assert.fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d)); - } - } catch (NoSuchMethodException ex) { - // not supported, no reason to test for it + 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) { + Assert.fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d)); } } From c653ae33d4d996bbedec220973f559e48b8680e7 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 20 Jan 2015 11:59:35 +0100 Subject: [PATCH 059/857] Fixed file comment, larger timeout for a test --- .../rx/internal/util/BackpressureDrainManager.java | 13 ++++++------- .../operators/OperatorOnBackpressureBufferTest.java | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/internal/util/BackpressureDrainManager.java b/src/main/java/rx/internal/util/BackpressureDrainManager.java index f898805e33..7e521d0bba 100644 --- a/src/main/java/rx/internal/util/BackpressureDrainManager.java +++ b/src/main/java/rx/internal/util/BackpressureDrainManager.java @@ -1,19 +1,18 @@ -/* - * Copyright 2011 David Karnok - * +/** + * 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 - * + * + * 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.concurrent.atomic.AtomicLongFieldUpdater; diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java index 34cb73cfc0..ff2d8e0c6f 100644 --- a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java @@ -47,7 +47,7 @@ public void testNoBackpressureSupport() { ts.assertNoErrors(); } - @Test(timeout = 500) + @Test(timeout = 2000) public void testFixBackpressureWithBuffer() throws InterruptedException { final CountDownLatch l1 = new CountDownLatch(100); final CountDownLatch l2 = new CountDownLatch(150); From 7020bcc8a3c19da6b4cf20a1e16befcf8fa16845 Mon Sep 17 00:00:00 2001 From: Roman Mazur Date: Mon, 10 Nov 2014 11:41:29 +0200 Subject: [PATCH 060/857] Unsubscribe when thread is interrupted --- .../operators/BlockingOperatorLatest.java | 1 + .../operators/BlockingOperatorNext.java | 1 + .../operators/BlockingOperatorToIterator.java | 4 +- .../rx/observables/BlockingObservable.java | 7 +- .../observables/BlockingObservableTest.java | 132 ++++++++++++++++++ 5 files changed, 142 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/BlockingOperatorLatest.java b/src/main/java/rx/internal/operators/BlockingOperatorLatest.java index 3679db35aa..c5f90f3828 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorLatest.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorLatest.java @@ -96,6 +96,7 @@ public boolean hasNext() { try { notify.acquire(); } catch (InterruptedException ex) { + unsubscribe(); Thread.currentThread().interrupt(); iNotif = Notification.createOnError(ex); throw Exceptions.propagate(ex); diff --git a/src/main/java/rx/internal/operators/BlockingOperatorNext.java b/src/main/java/rx/internal/operators/BlockingOperatorNext.java index 3ed10fba5d..600f5b418d 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorNext.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorNext.java @@ -117,6 +117,7 @@ private boolean moveToNext() { } throw new IllegalStateException("Should not reach here"); } catch (InterruptedException e) { + observer.unsubscribe(); Thread.currentThread().interrupt(); error = e; throw Exceptions.propagate(error); diff --git a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java index 7aadd8cddf..da63d9e227 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java @@ -23,6 +23,7 @@ import rx.Notification; import rx.Observable; import rx.Subscriber; +import rx.Subscription; import rx.exceptions.Exceptions; /** @@ -49,7 +50,7 @@ public static Iterator toIterator(Observable source) { final BlockingQueue> notifications = new LinkedBlockingQueue>(); // using subscribe instead of unsafeSubscribe since this is a BlockingObservable "final subscribe" - source.materialize().subscribe(new Subscriber>() { + final Subscription subscription = source.materialize().subscribe(new Subscriber>() { @Override public void onCompleted() { // ignore @@ -94,6 +95,7 @@ private Notification take() { try { return notifications.take(); } catch (InterruptedException e) { + subscription.unsubscribe(); throw Exceptions.propagate(e); } } diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 851ce3c315..477bf54bc4 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -23,6 +23,7 @@ import rx.Observable; import rx.Subscriber; +import rx.Subscription; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Functions; @@ -98,7 +99,7 @@ public void forEach(final Action1 onNext) { * Use 'subscribe' instead of 'unsafeSubscribe' for Rx contract behavior * as this is the final subscribe in the chain. */ - o.subscribe(new Subscriber() { + Subscription subscription = o.subscribe(new Subscriber() { @Override public void onCompleted() { latch.countDown(); @@ -127,6 +128,7 @@ public void onNext(T args) { try { latch.await(); } catch (InterruptedException e) { + subscription.unsubscribe(); // set the interrupted flag again so callers can still get it // for more information see https://github.com/ReactiveX/RxJava/pull/147#issuecomment-13624780 Thread.currentThread().interrupt(); @@ -438,7 +440,7 @@ private T blockForSingle(final Observable observable) { final AtomicReference returnException = new AtomicReference(); final CountDownLatch latch = new CountDownLatch(1); - observable.subscribe(new Subscriber() { + Subscription subscription = observable.subscribe(new Subscriber() { @Override public void onCompleted() { latch.countDown(); @@ -459,6 +461,7 @@ public void onNext(final T item) { try { latch.await(); } catch (InterruptedException e) { + subscription.unsubscribe(); Thread.currentThread().interrupt(); throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); } diff --git a/src/test/java/rx/observables/BlockingObservableTest.java b/src/test/java/rx/observables/BlockingObservableTest.java index 65affa4ff9..95b0e21ac5 100644 --- a/src/test/java/rx/observables/BlockingObservableTest.java +++ b/src/test/java/rx/observables/BlockingObservableTest.java @@ -410,4 +410,136 @@ public void call() { } assertTrue("Timeout means `unsubscribe` is not called", unsubscribe.await(30, TimeUnit.SECONDS)); } + + @Test + public void testUnsubscribeFromSingleWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("single()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.single(); + } + }); + } + + @Test + public void testUnsubscribeFromForEachWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("forEach()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.forEach(new Action1() { + @Override + public void call(final Void aVoid) { + // nothing + } + }); + } + }); + } + + @Test + public void testUnsubscribeFromFirstWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("first()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.first(); + } + }); + } + + @Test + public void testUnsubscribeFromLastWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("last()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.last(); + } + }); + } + + @Test + public void testUnsubscribeFromLatestWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("latest()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.latest().iterator().next(); + } + }); + } + + @Test + public void testUnsubscribeFromNextWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("next()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.next().iterator().next(); + } + }); + } + + @Test + public void testUnsubscribeFromGetIteratorWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("getIterator()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.getIterator().next(); + } + }); + } + + @Test + public void testUnsubscribeFromToIterableWhenInterrupted() throws InterruptedException { + new InterruptionTests().assertUnsubscribeIsInvoked("toIterable()", new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.toIterable().iterator().next(); + } + }); + } + + /** Utilities set for interruption behaviour tests. */ + private static class InterruptionTests { + + private boolean isUnSubscribed; + private RuntimeException error; + private CountDownLatch latch = new CountDownLatch(1); + + private Observable createObservable() { + return Observable.never().doOnUnsubscribe(new Action0() { + @Override + public void call() { + isUnSubscribed = true; + } + }); + } + + private void startBlockingAndInterrupt(final Action1> blockingAction) { + Thread subscriptionThread = new Thread() { + @Override + public void run() { + try { + blockingAction.call(createObservable().toBlocking()); + } catch (RuntimeException e) { + if (!(e.getCause() instanceof InterruptedException)) { + error = e; + } + } + latch.countDown(); + } + }; + subscriptionThread.start(); + subscriptionThread.interrupt(); + } + + void assertUnsubscribeIsInvoked(final String method, final Action1> blockingAction) + throws InterruptedException { + startBlockingAndInterrupt(blockingAction); + assertTrue("Timeout means interruption is not performed", latch.await(30, TimeUnit.SECONDS)); + if (error != null) { + throw error; + } + assertTrue("'unsubscribe' is not invoked when thread is interrupted for " + method, isUnSubscribed); + } + + } + } From e8fa3a5d84493520671aab4c6fc6eb1e198e6ebe Mon Sep 17 00:00:00 2001 From: zsxwing Date: Tue, 20 Jan 2015 20:48:51 +0800 Subject: [PATCH 061/857] Remove the execute permission from source files --- src/main/java/rx/exceptions/OnErrorThrowable.java | 0 src/test/java/rx/exceptions/OnNextValueTest.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 src/main/java/rx/exceptions/OnErrorThrowable.java mode change 100755 => 100644 src/test/java/rx/exceptions/OnNextValueTest.java diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java old mode 100755 new mode 100644 diff --git a/src/test/java/rx/exceptions/OnNextValueTest.java b/src/test/java/rx/exceptions/OnNextValueTest.java old mode 100755 new mode 100644 From b73a2028b03a947d63cb18275c8020b6f9a43ce2 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 20 Jan 2015 16:04:34 +0100 Subject: [PATCH 062/857] Subject state information methods & bounded ReplaySubject termination fix --- .../internal/operators/NotificationLite.java | 17 ++ src/main/java/rx/subjects/AsyncSubject.java | 57 ++++ .../java/rx/subjects/BehaviorSubject.java | 55 ++++ src/main/java/rx/subjects/PublishSubject.java | 29 ++ src/main/java/rx/subjects/ReplaySubject.java | 215 ++++++++++++++- .../operators/NotificationLiteTest.java | 19 +- .../internal/operators/OperatorMergeTest.java | 2 +- .../java/rx/subjects/AsyncSubjectTest.java | 66 +++++ .../java/rx/subjects/BehaviorSubjectTest.java | 93 +++++++ .../java/rx/subjects/PublishSubjectTest.java | 53 ++++ .../ReplaySubjectBoundedConcurrencyTest.java | 150 ++++++++++- .../ReplaySubjectConcurrencyTest.java | 49 ++++ .../java/rx/subjects/ReplaySubjectTest.java | 252 ++++++++++++++++++ 13 files changed, 1042 insertions(+), 15 deletions(-) diff --git a/src/main/java/rx/internal/operators/NotificationLite.java b/src/main/java/rx/internal/operators/NotificationLite.java index b3d1e5b446..cd223e6784 100644 --- a/src/main/java/rx/internal/operators/NotificationLite.java +++ b/src/main/java/rx/internal/operators/NotificationLite.java @@ -176,6 +176,23 @@ public boolean isError(Object n) { return n instanceof OnErrorSentinel; } + /** + * Indicates whether or not the lite notification represents a wrapped {@code null} {@code onNext} event. + * @param n the lite notification + * @return {@code true} if {@code n} represents a wrapped {@code null} {@code onNext} event, {@code false} otherwise + */ + public boolean isNull(Object n) { + return n == ON_NEXT_NULL_SENTINEL; + } + + /** + * Indicates whether or not the lite notification represents an {@code onNext} event. + * @param n the lite notification + * @return {@code true} if {@code n} represents an {@code onNext} event, {@code false} otherwise + */ + public boolean isNext(Object n) { + return n != null && !isError(n) && !isCompleted(n); + } /** * Indicates which variety a particular lite notification is. If you need something more complex than * simply calling the right method on an {@link Observer} then you can use this method to get the diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index fd2fa97cd4..3bce5e92a8 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -140,4 +140,61 @@ public void onNext(T v) { public boolean hasObservers() { return state.observers().length > 0; } + /** + * Check if the Subject has a value. + *

Use the {@link #getValue()} method to retrieve such a value. + *

Note that unless {@link #hasCompleted()} or {@link #hasThrowable()} returns true, the value + * retrieved by {@code getValue()} may get outdated. + * @return true if and only if the subject has some value but not an error + */ + public boolean hasValue() { + Object v = lastValue; + Object o = state.get(); + return !nl.isError(o) && nl.isNext(v); + } + /** + * Check if the Subject has terminated with an exception. + * @return true if the subject has received a throwable through {@code onError}. + */ + public boolean hasThrowable() { + Object o = state.get(); + return nl.isError(o); + } + /** + * Check if the Subject has terminated normally. + * @return true if the subject completed normally via {@code onCompleted()} + */ + public boolean hasCompleted() { + Object o = state.get(); + return o != null && !nl.isError(o); + } + /** + * Returns the current value of the Subject if there is such a value and + * the subject hasn't terminated with an exception. + *

The can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} + * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an + * exception or the Subject terminated without receiving any value. + * @return the current value or {@code null} if the Subject doesn't have a value, + * has terminated with an exception or has an actual {@code null} as a value. + */ + public T getValue() { + Object v = lastValue; + Object o = state.get(); + if (!nl.isError(o) && nl.isNext(v)) { + return nl.getValue(v); + } + return null; + } + /** + * Returns the Throwable that terminated the Subject. + * @return the Throwable that terminated the Subject or {@code null} if the + * subject hasn't terminated yet or it terminated normally. + */ + public Throwable getThrowable() { + Object o = state.get(); + if (nl.isError(o)) { + return nl.getError(o); + } + return null; + } } diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index 0b9f446fe0..937116f5d4 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -176,4 +176,59 @@ public void onNext(T v) { public boolean hasObservers() { return state.observers().length > 0; } + /** + * Check if the Subject has a value. + *

Use the {@link #getValue()} method to retrieve such a value. + *

Note that unless {@link #hasCompleted()} or {@link #hasThrowable()} returns true, the value + * retrieved by {@code getValue()} may get outdated. + * @return true if and only if the subject has some value and hasn't terminated yet. + */ + public boolean hasValue() { + Object o = state.get(); + return nl.isNext(o); + } + /** + * Check if the Subject has terminated with an exception. + * @return true if the subject has received a throwable through {@code onError}. + */ + public boolean hasThrowable() { + Object o = state.get(); + return nl.isError(o); + } + /** + * Check if the Subject has terminated normally. + * @return true if the subject completed normally via {@code onCompleted()} + */ + public boolean hasCompleted() { + Object o = state.get(); + return nl.isCompleted(o); + } + /** + * Returns the current value of the Subject if there is such a value and + * the subject hasn't terminated yet. + *

The can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} + * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an + * exception or the Subject terminated (with or without receiving any value). + * @return the current value or {@code null} if the Subject doesn't have a value, + * has terminated or has an actual {@code null} as a valid value. + */ + public T getValue() { + Object o = state.get(); + if (nl.isNext(o)) { + return nl.getValue(o); + } + return null; + } + /** + * Returns the Throwable that terminated the Subject. + * @return the Throwable that terminated the Subject or {@code null} if the + * subject hasn't terminated yet or it terminated normally. + */ + public Throwable getThrowable() { + Object o = state.get(); + if (nl.isError(o)) { + return nl.getError(o); + } + return null; + } } diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index 50949395cb..dd575151b2 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -126,4 +126,33 @@ public void onNext(T v) { public boolean hasObservers() { return state.observers().length > 0; } + + /** + * Check if the Subject has terminated with an exception. + * @return true if the subject has received a throwable through {@code onError}. + */ + public boolean hasThrowable() { + Object o = state.get(); + return nl.isError(o); + } + /** + * Check if the Subject has terminated normally. + * @return true if the subject completed normally via {@code onCompleted} + */ + public boolean hasCompleted() { + Object o = state.get(); + return o != null && !nl.isError(o); + } + /** + * Returns the Throwable that terminated the Subject. + * @return the Throwable that terminated the Subject or {@code null} if the + * subject hasn't terminated yet or it terminated normally. + */ + public Throwable getThrowable() { + Object o = state.get(); + if (nl.isError(o)) { + return nl.getError(o); + } + return null; + } } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index a137dd6c4d..fe93b68ce4 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -15,6 +15,7 @@ */ package rx.subjects; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -465,7 +466,7 @@ public UnboundedReplayState(int initialCapacity) { public void next(T n) { if (!terminated) { list.add(nl.next(n)); - INDEX_UPDATER.getAndIncrement(this); + INDEX_UPDATER.getAndIncrement(this); // release index } } @@ -478,7 +479,7 @@ public void complete() { if (!terminated) { terminated = true; list.add(nl.completed()); - INDEX_UPDATER.getAndIncrement(this); + INDEX_UPDATER.getAndIncrement(this); // release index } } @Override @@ -486,7 +487,7 @@ public void error(Throwable e) { if (!terminated) { terminated = true; list.add(nl.error(e)); - INDEX_UPDATER.getAndIncrement(this); + INDEX_UPDATER.getAndIncrement(this); // release index } } @@ -530,6 +531,39 @@ public Integer replayObserverFromIndex(Integer idx, SubjectObserver o public Integer replayObserverFromIndexTest(Integer idx, SubjectObserver observer, long now) { return replayObserverFromIndex(idx, observer); } + + @Override + public int size() { + int idx = index; // aquire + if (idx > 0) { + Object o = list.get(idx - 1); + if (nl.isCompleted(o) || nl.isError(o)) { + return idx - 1; // do not report a terminal event as part of size + } + } + return idx; + } + @Override + public boolean isEmpty() { + return size() == 0; + } + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + int s = size(); + if (s > 0) { + if (s > a.length) { + a = (T[])Array.newInstance(a.getClass().getComponentType(), s); + } + for (int i = 0; i < s; i++) { + a[i] = (T)list.get(i); + } + if (s < a.length - 1) { + a[s] = null; + } + } + return a; + } } @@ -566,10 +600,8 @@ public void next(T value) { public void complete() { if (!terminated) { terminated = true; - // don't evict the terminal value - evictionPolicy.evict(list); - // so add it later list.addLast(enterTransform.call(nl.completed())); + evictionPolicy.evictFinal(list); tail = list.tail; } @@ -578,10 +610,9 @@ public void complete() { public void error(Throwable e) { if (!terminated) { terminated = true; - // don't evict the terminal value - evictionPolicy.evict(list); - // so add it later list.addLast(enterTransform.call(nl.error(e))); + // don't evict the terminal value + evictionPolicy.evictFinal(list); tail = list.tail; } } @@ -644,6 +675,54 @@ public NodeList.Node replayObserverFromIndexTest( public boolean terminated() { return terminated; } + + @Override + public int size() { + int size = 0; + NodeList.Node l = head(); + NodeList.Node next = l.next; + while (next != null) { + size++; + l = next; + next = next.next; + } + if (l.value != null) { + Object value = leaveTransform.call(l.value); + if (value != null && (nl.isError(value) || nl.isCompleted(value))) { + return size - 1; + } + } + return size; + } + @Override + public boolean isEmpty() { + NodeList.Node l = head(); + NodeList.Node next = l.next; + if (next == null) { + return true; + } + Object value = leaveTransform.call(next.value); + return nl.isError(value) || nl.isCompleted(value); + } + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + List list = new ArrayList(); + NodeList.Node l = head(); + NodeList.Node next = l.next; + while (next != null) { + Object o = leaveTransform.call(next.value); + + if (next.next == null && (nl.isError(o) || nl.isCompleted(o))) { + break; + } else { + list.add((T)o); + } + l = next; + next = next.next; + } + return list.toArray(a); + } } // ************** @@ -694,6 +773,22 @@ I replayObserverFromIndexTest( * Add an OnCompleted exception and terminate the subject */ void complete(); + /** + * @return the number of non-terminal values in the replay buffer. + */ + int size(); + /** + * @return true if the replay buffer is empty of non-terminal values + */ + boolean isEmpty(); + + /** + * Copy the current values (minus any terminal value) from the buffer into the array + * or create a new array if there isn't enough room. + * @param a the array to fill in + * @return the array or a new array containing the current values + */ + T[] toArray(T[] a); } /** Interface to manage eviction checking. */ @@ -706,10 +801,16 @@ interface EvictionPolicy { */ boolean test(Object value, long now); /** - * Evict values from the list - * @param list + * Evict values from the list. + * @param list the node list */ void evict(NodeList list); + /** + * Evict values from the list except the very last which is considered + * a terminal event + * @param list the node list + */ + void evictFinal(NodeList list); } @@ -738,6 +839,13 @@ public void evict(NodeList t1) { public boolean test(Object value, long now) { return true; // size gets never stale } + + @Override + public void evictFinal(NodeList t1) { + while (t1.size() > maxSize + 1) { + t1.removeFirst(); + } + } } /** * Remove elements from the beginning of the list if the Timestamped value is older than @@ -764,6 +872,19 @@ public void evict(NodeList t1) { } } } + + @Override + public void evictFinal(NodeList t1) { + long now = scheduler.now(); + while (t1.size > 1) { + NodeList.Node n = t1.head.next; + if (test(n.value, now)) { + t1.removeFirst(); + } else { + break; + } + } + } @Override public boolean test(Object value, long now) { @@ -789,6 +910,12 @@ public void evict(NodeList t1) { first.evict(t1); second.evict(t1); } + + @Override + public void evictFinal(NodeList t1) { + first.evictFinal(t1); + second.evictFinal(t1); + } @Override public boolean test(Object value, long now) { @@ -926,6 +1053,70 @@ public boolean test(Object value, long now) { @Override public void evict(NodeList list) { } - + @Override + public void evictFinal(NodeList list) { + } } + /** + * Check if the Subject has terminated with an exception. + * @return true if the subject has received a throwable through {@code onError}. + */ + public boolean hasThrowable() { + NotificationLite nl = ssm.nl; + Object o = ssm.get(); + return nl.isError(o); + } + /** + * Check if the Subject has terminated normally. + * @return true if the subject completed normally via {@code onCompleted} + */ + public boolean hasCompleted() { + NotificationLite nl = ssm.nl; + Object o = ssm.get(); + return o != null && !nl.isError(o); + } + /** + * Returns the Throwable that terminated the Subject. + * @return the Throwable that terminated the Subject or {@code null} if the + * subject hasn't terminated yet or it terminated normally. + */ + public Throwable getThrowable() { + NotificationLite nl = ssm.nl; + Object o = ssm.get(); + if (nl.isError(o)) { + return nl.getError(o); + } + return null; + } + /** + * Returns the current number of items (non-terminal events) available for replay. + * @return the number of items available + */ + public int size() { + return state.size(); + } + /** + * @return true if the Subject holds at least one non-terminal event available for replay + */ + public boolean hasAnyValue() { + return !state.isEmpty(); + } + /** An empty array to trigger getValues() to return a new array. */ + private static final Object[] EMPTY_ARRAY = new Object[0]; + /** + * @return returns a snapshot of the currently buffered non-terminal events. + */ + @SuppressWarnings("unchecked") + public Object[] getValues() { + return state.toArray((T[])EMPTY_ARRAY); + } + /** + * Returns a snapshot of the currently buffered non-terminal events into + * the provided {@code a} array or creates a new array if it has not enough capacity. + * @param a the array to fill in + * @return the array {@code a} if it had enough capacity or a new array containing the available values + */ + public T[] getValues(T[] a) { + return state.toArray(a); + } } diff --git a/src/test/java/rx/internal/operators/NotificationLiteTest.java b/src/test/java/rx/internal/operators/NotificationLiteTest.java index 3d27d39498..9b9f3f7661 100644 --- a/src/test/java/rx/internal/operators/NotificationLiteTest.java +++ b/src/test/java/rx/internal/operators/NotificationLiteTest.java @@ -19,6 +19,8 @@ import org.junit.Test; +import rx.exceptions.TestException; + public class NotificationLiteTest { @@ -34,5 +36,20 @@ public void testComplete() { assertEquals("Hello", on.getValue(n)); } - + @Test + public void testValueKind() { + NotificationLite on = NotificationLite.instance(); + + assertTrue(on.isNull(on.next(null))); + assertFalse(on.isNull(on.next(1))); + assertFalse(on.isNull(on.error(new TestException()))); + assertFalse(on.isNull(on.completed())); + assertFalse(on.isNull(null)); + + assertTrue(on.isNext(on.next(null))); + assertTrue(on.isNext(on.next(1))); + assertFalse(on.isNext(on.completed())); + assertFalse(on.isNext(null)); + assertFalse(on.isNext(on.error(new TestException()))); + } } diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 48e7680581..46a5543a8b 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -494,7 +494,7 @@ public void call() { }); } - @Test + @Test(timeout = 10000) public void testConcurrency() { Observable o = Observable.range(1, 10000).subscribeOn(Schedulers.newThread()); diff --git a/src/test/java/rx/subjects/AsyncSubjectTest.java b/src/test/java/rx/subjects/AsyncSubjectTest.java index 433531ab6c..c4287fc363 100644 --- a/src/test/java/rx/subjects/AsyncSubjectTest.java +++ b/src/test/java/rx/subjects/AsyncSubjectTest.java @@ -16,6 +16,9 @@ package rx.subjects; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; @@ -36,6 +39,7 @@ import rx.Subscription; import rx.exceptions.CompositeException; import rx.exceptions.OnErrorNotImplementedException; +import rx.exceptions.TestException; import rx.functions.Action1; import rx.observers.TestSubscriber; @@ -330,4 +334,66 @@ public void testOnErrorThrowsDoesntPreventDelivery2() { assertEquals(1, ts.getOnErrorEvents().size()); } + @Test + public void testCurrentStateMethodsNormal() { + AsyncSubject as = AsyncSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + + as.onCompleted(); + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void testCurrentStateMethodsEmpty() { + AsyncSubject as = AsyncSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onCompleted(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + @Test + public void testCurrentStateMethodsError() { + AsyncSubject as = AsyncSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertFalse(as.hasValue()); + assertTrue(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertTrue(as.getThrowable() instanceof TestException); + } } diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index 76ae5bd723..0036ca5d7a 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -17,6 +17,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.inOrder; @@ -35,6 +37,7 @@ import rx.*; import rx.exceptions.CompositeException; import rx.exceptions.OnErrorNotImplementedException; +import rx.exceptions.TestException; import rx.functions.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -482,4 +485,94 @@ public void onNext(Object t) { } } } + + @Test + public void testCurrentStateMethodsNormalEmptyStart() { + BehaviorSubject as = BehaviorSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + + as.onCompleted(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void testCurrentStateMethodsNormalSomeStart() { + BehaviorSubject as = BehaviorSubject.create((Object)1); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(2); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertEquals(2, as.getValue()); + assertNull(as.getThrowable()); + + as.onCompleted(); + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void testCurrentStateMethodsEmpty() { + BehaviorSubject as = BehaviorSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onCompleted(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + @Test + public void testCurrentStateMethodsError() { + BehaviorSubject as = BehaviorSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertFalse(as.hasValue()); + assertTrue(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getValue()); + assertTrue(as.getThrowable() instanceof TestException); + } } diff --git a/src/test/java/rx/subjects/PublishSubjectTest.java b/src/test/java/rx/subjects/PublishSubjectTest.java index 2c01d18f54..f8bc989112 100644 --- a/src/test/java/rx/subjects/PublishSubjectTest.java +++ b/src/test/java/rx/subjects/PublishSubjectTest.java @@ -16,6 +16,9 @@ package rx.subjects; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.inOrder; @@ -36,6 +39,7 @@ import rx.Subscription; import rx.exceptions.CompositeException; import rx.exceptions.OnErrorNotImplementedException; +import rx.exceptions.TestException; import rx.functions.Action1; import rx.functions.Func1; import rx.observers.TestSubscriber; @@ -395,4 +399,53 @@ public void testOnErrorThrowsDoesntPreventDelivery2() { // even though the onError above throws we should still receive it on the other subscriber assertEquals(1, ts.getOnErrorEvents().size()); } + @Test + public void testCurrentStateMethodsNormal() { + PublishSubject as = PublishSubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onCompleted(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getThrowable()); + } + + @Test + public void testCurrentStateMethodsEmpty() { + PublishSubject as = PublishSubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onCompleted(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getThrowable()); + } + @Test + public void testCurrentStateMethodsError() { + PublishSubject as = PublishSubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertTrue(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertTrue(as.getThrowable() instanceof TestException); + } } diff --git a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java index 9924a8b71e..9bc3e47823 100644 --- a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java @@ -21,7 +21,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -29,7 +31,6 @@ import rx.*; import rx.Observable.OnSubscribe; -import rx.Subscriber; import rx.functions.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -405,4 +406,151 @@ public void call() { } } } + @Test(timeout = 5000) + public void testConcurrentSizeAndHasAnyValue() throws InterruptedException { + final ReplaySubject rs = ReplaySubject.createUnbounded(); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onCompleted(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + int lastSize = 0; + for (; !rs.hasThrowable() && !rs.hasCompleted();) { + int size = rs.size(); + boolean hasAny = rs.hasAnyValue(); + Object[] values = rs.getValues(); + if (size < lastSize) { + Assert.fail("Size decreased! " + lastSize + " -> " + size); + } + if ((size > 0) && !hasAny) { + Assert.fail("hasAnyValue reports emptyness but size doesn't"); + } + if (size > values.length) { + Assert.fail("Got fewer values than size! " + size + " -> " + values.length); + } + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + lastSize = size; + } + + t.join(); + } + @Test(timeout = 5000) + public void testConcurrentSizeAndHasAnyValueBounded() throws InterruptedException { + final ReplaySubject rs = ReplaySubject.createWithSize(3); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onCompleted(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (; !rs.hasThrowable() && !rs.hasCompleted();) { + rs.size(); // can't use value so just call to detect hangs + rs.hasAnyValue(); // can't use value so just call to detect hangs + Object[] values = rs.getValues(); + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + } + + t.join(); + } + @Test(timeout = 5000) + public void testConcurrentSizeAndHasAnyValueTimeBounded() throws InterruptedException { + final ReplaySubject rs = ReplaySubject.createWithTime(1, TimeUnit.MILLISECONDS, Schedulers.computation()); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + if (i % 10000 == 0) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + return; + } + } + } + rs.onCompleted(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (; !rs.hasThrowable() && !rs.hasCompleted();) { + rs.size(); // can't use value so just call to detect hangs + rs.hasAnyValue(); // can't use value so just call to detect hangs + Object[] values = rs.getValues(); + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + } + + t.join(); + } } \ No newline at end of file diff --git a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java index 2120a49373..cc99eb737d 100644 --- a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java @@ -407,4 +407,53 @@ public void call() { worker.unsubscribe(); } } + @Test(timeout = 5000) + public void testConcurrentSizeAndHasAnyValue() throws InterruptedException { + final ReplaySubject rs = ReplaySubject.create(); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onCompleted(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + int lastSize = 0; + for (; !rs.hasThrowable() && !rs.hasCompleted();) { + int size = rs.size(); + boolean hasAny = rs.hasAnyValue(); + Object[] values = rs.getValues(); + if (size < lastSize) { + Assert.fail("Size decreased! " + lastSize + " -> " + size); + } + if ((size > 0) && !hasAny) { + Assert.fail("hasAnyValue reports emptyness but size doesn't"); + } + if (size > values.length) { + Assert.fail("Got fewer values than size! " + size + " -> " + values.length); + } + lastSize = size; + } + + t.join(); + } } diff --git a/src/test/java/rx/subjects/ReplaySubjectTest.java b/src/test/java/rx/subjects/ReplaySubjectTest.java index d6ff19ab9c..bd01901bc1 100644 --- a/src/test/java/rx/subjects/ReplaySubjectTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectTest.java @@ -15,7 +15,11 @@ */ package rx.subjects; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.inOrder; @@ -25,6 +29,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +44,7 @@ import rx.Subscription; import rx.exceptions.CompositeException; import rx.exceptions.OnErrorNotImplementedException; +import rx.exceptions.TestException; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -637,4 +643,250 @@ public void testOnErrorThrowsDoesntPreventDelivery2() { // even though the onError above throws we should still receive it on the other subscriber assertEquals(1, ts.getOnErrorEvents().size()); } + + @Test + public void testCurrentStateMethodsNormal() { + ReplaySubject as = ReplaySubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onCompleted(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getThrowable()); + } + + @Test + public void testCurrentStateMethodsEmpty() { + ReplaySubject as = ReplaySubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onCompleted(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasCompleted()); + assertNull(as.getThrowable()); + } + @Test + public void testCurrentStateMethodsError() { + ReplaySubject as = ReplaySubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertTrue(as.hasThrowable()); + assertFalse(as.hasCompleted()); + assertTrue(as.getThrowable() instanceof TestException); + } + @Test + public void testSizeAndHasAnyValueUnbounded() { + ReplaySubject rs = ReplaySubject.create(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onCompleted(); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + } + @Test + public void testSizeAndHasAnyValueEffectivelyUnbounded() { + ReplaySubject rs = ReplaySubject.createUnbounded(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onCompleted(); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + } + + @Test + public void testSizeAndHasAnyValueUnboundedError() { + ReplaySubject rs = ReplaySubject.create(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onError(new TestException()); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + } + @Test + public void testSizeAndHasAnyValueEffectivelyUnboundedError() { + ReplaySubject rs = ReplaySubject.createUnbounded(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + + rs.onError(new TestException()); + + assertEquals(2, rs.size()); + assertTrue(rs.hasAnyValue()); + } + + @Test + public void testSizeAndHasAnyValueUnboundedEmptyError() { + ReplaySubject rs = ReplaySubject.create(); + + rs.onError(new TestException()); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + } + @Test + public void testSizeAndHasAnyValueEffectivelyUnboundedEmptyError() { + ReplaySubject rs = ReplaySubject.createUnbounded(); + + rs.onError(new TestException()); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + } + + @Test + public void testSizeAndHasAnyValueUnboundedEmptyCompleted() { + ReplaySubject rs = ReplaySubject.create(); + + rs.onCompleted(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + } + @Test + public void testSizeAndHasAnyValueEffectivelyUnboundedEmptyCompleted() { + ReplaySubject rs = ReplaySubject.createUnbounded(); + + rs.onCompleted(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + } + + @Test + public void testSizeAndHasAnyValueSizeBounded() { + ReplaySubject rs = ReplaySubject.createWithSize(1); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + + for (int i = 0; i < 1000; i++) { + rs.onNext(i); + + assertEquals(1, rs.size()); + assertTrue(rs.hasAnyValue()); + } + + rs.onCompleted(); + + assertEquals(1, rs.size()); + assertTrue(rs.hasAnyValue()); + } + + @Test + public void testSizeAndHasAnyValueTimeBounded() { + TestScheduler ts = new TestScheduler(); + ReplaySubject rs = ReplaySubject.createWithTime(1, TimeUnit.SECONDS, ts); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + + for (int i = 0; i < 1000; i++) { + rs.onNext(i); + ts.advanceTimeBy(2, TimeUnit.SECONDS); + assertEquals(1, rs.size()); + assertTrue(rs.hasAnyValue()); + } + + rs.onCompleted(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasAnyValue()); + } + @Test + public void testGetValues() { + ReplaySubject rs = ReplaySubject.create(); + Object[] expected = new Object[10]; + for (int i = 0; i < expected.length; i++) { + expected[i] = i; + rs.onNext(i); + assertArrayEquals(Arrays.copyOf(expected, i + 1), rs.getValues()); + } + rs.onCompleted(); + + assertArrayEquals(expected, rs.getValues()); + + } + @Test + public void testGetValuesUnbounded() { + ReplaySubject rs = ReplaySubject.createUnbounded(); + Object[] expected = new Object[10]; + for (int i = 0; i < expected.length; i++) { + expected[i] = i; + rs.onNext(i); + assertArrayEquals(Arrays.copyOf(expected, i + 1), rs.getValues()); + } + rs.onCompleted(); + + assertArrayEquals(expected, rs.getValues()); + + } } From 6db3afc0679304d3c835e8581ff62a296adc980d Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Sun, 18 Jan 2015 17:02:46 -0800 Subject: [PATCH 063/857] Fixes NPEs reported in ReactiveX#1702 by synchronizing queue. Adds unit test for regression. --- .../rx/schedulers/TrampolineScheduler.java | 18 ++++---- .../schedulers/TrampolineSchedulerTest.java | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index a948276cc6..ecf737b42e 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -71,14 +71,17 @@ private Subscription enqueue(Action0 action, long execTime) { return Subscriptions.unsubscribed(); } final TimedAction timedAction = new TimedAction(action, execTime, COUNTER_UPDATER.incrementAndGet(TrampolineScheduler.this)); - queue.add(timedAction); + synchronized (queue) { + queue.add(timedAction); + } if (wip.getAndIncrement() == 0) { do { - TimedAction polled = queue.poll(); - // check for null as it could have been unsubscribed and removed - if (polled != null) { - polled.action.call(); + synchronized (queue) { + if (!queue.isEmpty()) { + TimedAction polled = queue.poll(); + polled.action.call(); + } } } while (wip.decrementAndGet() > 0); return Subscriptions.unsubscribed(); @@ -88,9 +91,8 @@ private Subscription enqueue(Action0 action, long execTime) { @Override public void call() { - PriorityQueue _q = queue; - if (_q != null) { - _q.remove(timedAction); + synchronized (queue) { + queue.remove(timedAction); } } diff --git a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java index c628da245f..a1bad56e34 100644 --- a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java +++ b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java @@ -18,13 +18,17 @@ import static org.junit.Assert.*; import java.util.*; +import java.util.concurrent.TimeUnit; import org.junit.Test; import rx.*; +import rx.Observer; import rx.Scheduler.Worker; import rx.Observable; import rx.functions.*; +import rx.observers.Observers; +import rx.observers.TestSubscriber; import rx.subscriptions.CompositeSubscription; public class TrampolineSchedulerTest extends AbstractSchedulerTests { @@ -95,6 +99,47 @@ public void call() { } } + /** + * This is a regression test for #1702. Concurrent work scheduling that is improperly synchronized can cause an + * action to be added or removed onto the priority queue during a poll, which can result in NPEs during queue + * sifting. While it is difficult to isolate the issue directly, we can easily trigger the behavior by spamming the + * trampoline with enqueue requests from multiple threads concurrently. + */ + @Test + public void testTrampolineWorkerHandlesConcurrentScheduling() { + final Worker trampolineWorker = Schedulers.trampoline().createWorker(); + final Observer observer = Observers.empty(); + final TestSubscriber ts = new TestSubscriber(observer); + + // Spam the trampoline with actions. + Observable.range(0, 50) + .flatMap(new Func1>() { + + @Override + public Observable call(Integer count) { + return Observable.interval(1, TimeUnit.MICROSECONDS).map( + new Func1() { + + @Override + public Subscription call(Long count) { + return trampolineWorker.schedule(new Action0() { + + @Override + public void call() {} + + }); + } + + }).limit(100); + } + + }) + .subscribeOn(Schedulers.computation()) + .subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + } + private static Worker doWorkOnNewTrampoline(final String key, final ArrayList workDone) { Worker worker = Schedulers.trampoline().createWorker(); worker.schedule(new Action0() { From 14fcc22ec431e87b88f99a7d5498c321127dd48d Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Tue, 20 Jan 2015 13:13:55 -0800 Subject: [PATCH 064/857] Use unsubscribed check instead of a null check. --- .../java/rx/subscriptions/CompositeSubscription.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index d69a413554..777b18f04d 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -1,12 +1,12 @@ /** * 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. @@ -55,8 +55,8 @@ public synchronized boolean isUnsubscribed() { * the {@link Subscription} to add */ public void add(final Subscription s) { - if (s == null) { - throw new NullPointerException("Added Subscription cannot be null."); + if (s.isUnsubscribed()) { + return; } Subscription unsubscribe = null; synchronized (this) { From 412828dbfc41dc8bd3a2323bfb883712d5445d44 Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Sun, 18 Jan 2015 17:02:46 -0800 Subject: [PATCH 065/857] Fixes NPEs reported in ReactiveX#1702 by synchronizing queue. Adds unit test for regression. --- .../rx/schedulers/TrampolineScheduler.java | 25 ++++++----- .../schedulers/TrampolineSchedulerTest.java | 45 +++++++++++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index a948276cc6..1843bcc524 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -1,12 +1,12 @@ /** * 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. @@ -71,14 +71,18 @@ private Subscription enqueue(Action0 action, long execTime) { return Subscriptions.unsubscribed(); } final TimedAction timedAction = new TimedAction(action, execTime, COUNTER_UPDATER.incrementAndGet(TrampolineScheduler.this)); - queue.add(timedAction); + synchronized (queue) { + queue.add(timedAction); + } if (wip.getAndIncrement() == 0) { do { - TimedAction polled = queue.poll(); - // check for null as it could have been unsubscribed and removed + TimedAction polled; + synchronized (queue) { + polled = queue.poll(); + } if (polled != null) { - polled.action.call(); + polled.action.call(); } } while (wip.decrementAndGet() > 0); return Subscriptions.unsubscribed(); @@ -88,9 +92,8 @@ private Subscription enqueue(Action0 action, long execTime) { @Override public void call() { - PriorityQueue _q = queue; - if (_q != null) { - _q.remove(timedAction); + synchronized (queue) { + queue.remove(timedAction); } } @@ -130,7 +133,7 @@ public int compareTo(TimedAction that) { return result; } } - + // because I can't use Integer.compare from Java 7 private static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); diff --git a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java index c628da245f..a1bad56e34 100644 --- a/src/test/java/rx/schedulers/TrampolineSchedulerTest.java +++ b/src/test/java/rx/schedulers/TrampolineSchedulerTest.java @@ -18,13 +18,17 @@ import static org.junit.Assert.*; import java.util.*; +import java.util.concurrent.TimeUnit; import org.junit.Test; import rx.*; +import rx.Observer; import rx.Scheduler.Worker; import rx.Observable; import rx.functions.*; +import rx.observers.Observers; +import rx.observers.TestSubscriber; import rx.subscriptions.CompositeSubscription; public class TrampolineSchedulerTest extends AbstractSchedulerTests { @@ -95,6 +99,47 @@ public void call() { } } + /** + * This is a regression test for #1702. Concurrent work scheduling that is improperly synchronized can cause an + * action to be added or removed onto the priority queue during a poll, which can result in NPEs during queue + * sifting. While it is difficult to isolate the issue directly, we can easily trigger the behavior by spamming the + * trampoline with enqueue requests from multiple threads concurrently. + */ + @Test + public void testTrampolineWorkerHandlesConcurrentScheduling() { + final Worker trampolineWorker = Schedulers.trampoline().createWorker(); + final Observer observer = Observers.empty(); + final TestSubscriber ts = new TestSubscriber(observer); + + // Spam the trampoline with actions. + Observable.range(0, 50) + .flatMap(new Func1>() { + + @Override + public Observable call(Integer count) { + return Observable.interval(1, TimeUnit.MICROSECONDS).map( + new Func1() { + + @Override + public Subscription call(Long count) { + return trampolineWorker.schedule(new Action0() { + + @Override + public void call() {} + + }); + } + + }).limit(100); + } + + }) + .subscribeOn(Schedulers.computation()) + .subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + } + private static Worker doWorkOnNewTrampoline(final String key, final ArrayList workDone) { Worker worker = Schedulers.trampoline().createWorker(); worker.schedule(new Action0() { From 28ec4c794a8cb7d1f86c9725403d29b2e6130396 Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Tue, 20 Jan 2015 14:04:03 -0800 Subject: [PATCH 066/857] Don't block the action on the queue lock. --- src/main/java/rx/schedulers/TrampolineScheduler.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index 1d0788eeff..150fbf5ef9 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -79,11 +79,9 @@ private Subscription enqueue(Action0 action, long execTime) { do { TimedAction polled; synchronized (queue) { - if (!queue.isEmpty()) { - TimedAction polled = queue.poll(); - polled.action.call(); - } + polled = queue.poll(); } + polled.action.call(); } while (wip.decrementAndGet() > 0); return Subscriptions.unsubscribed(); } else { From 9544c6aa6416bd4686615af330e019d84e8d1aaf Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 21 Jan 2015 09:15:30 +0100 Subject: [PATCH 067/857] Added experimental annotations --- src/main/java/rx/subjects/AsyncSubject.java | 6 ++++++ src/main/java/rx/subjects/BehaviorSubject.java | 6 ++++++ src/main/java/rx/subjects/PublishSubject.java | 4 ++++ src/main/java/rx/subjects/ReplaySubject.java | 8 ++++++++ 4 files changed, 24 insertions(+) diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index 3bce5e92a8..d3d135346c 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -19,6 +19,7 @@ import java.util.List; import rx.Observer; +import rx.annotations.Experimental; import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; import rx.functions.Action1; @@ -147,6 +148,7 @@ public boolean hasObservers() { * retrieved by {@code getValue()} may get outdated. * @return true if and only if the subject has some value but not an error */ + @Experimental public boolean hasValue() { Object v = lastValue; Object o = state.get(); @@ -156,6 +158,7 @@ public boolean hasValue() { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ + @Experimental public boolean hasThrowable() { Object o = state.get(); return nl.isError(o); @@ -164,6 +167,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted()} */ + @Experimental public boolean hasCompleted() { Object o = state.get(); return o != null && !nl.isError(o); @@ -177,6 +181,7 @@ public boolean hasCompleted() { * @return the current value or {@code null} if the Subject doesn't have a value, * has terminated with an exception or has an actual {@code null} as a value. */ + @Experimental public T getValue() { Object v = lastValue; Object o = state.get(); @@ -190,6 +195,7 @@ public T getValue() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ + @Experimental public Throwable getThrowable() { Object o = state.get(); if (nl.isError(o)) { diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index 937116f5d4..fee30f0d99 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -20,6 +20,7 @@ import java.util.List; import rx.Observer; +import rx.annotations.Experimental; import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; import rx.functions.Action1; @@ -183,6 +184,7 @@ public boolean hasObservers() { * retrieved by {@code getValue()} may get outdated. * @return true if and only if the subject has some value and hasn't terminated yet. */ + @Experimental public boolean hasValue() { Object o = state.get(); return nl.isNext(o); @@ -191,6 +193,7 @@ public boolean hasValue() { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ + @Experimental public boolean hasThrowable() { Object o = state.get(); return nl.isError(o); @@ -199,6 +202,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted()} */ + @Experimental public boolean hasCompleted() { Object o = state.get(); return nl.isCompleted(o); @@ -212,6 +216,7 @@ public boolean hasCompleted() { * @return the current value or {@code null} if the Subject doesn't have a value, * has terminated or has an actual {@code null} as a valid value. */ + @Experimental public T getValue() { Object o = state.get(); if (nl.isNext(o)) { @@ -224,6 +229,7 @@ public T getValue() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ + @Experimental public Throwable getThrowable() { Object o = state.get(); if (nl.isError(o)) { diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index dd575151b2..a4d900ddb3 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -19,6 +19,7 @@ import java.util.List; import rx.Observer; +import rx.annotations.Experimental; import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; import rx.functions.Action1; @@ -131,6 +132,7 @@ public boolean hasObservers() { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ + @Experimental public boolean hasThrowable() { Object o = state.get(); return nl.isError(o); @@ -139,6 +141,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted} */ + @Experimental public boolean hasCompleted() { Object o = state.get(); return o != null && !nl.isError(o); @@ -148,6 +151,7 @@ public boolean hasCompleted() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ + @Experimental public Throwable getThrowable() { Object o = state.get(); if (nl.isError(o)) { diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index fe93b68ce4..73ddf8585a 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -23,6 +23,7 @@ import rx.Observer; import rx.Scheduler; +import rx.annotations.Experimental; import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; import rx.functions.Action1; @@ -1061,6 +1062,7 @@ public void evictFinal(NodeList list) { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ + @Experimental public boolean hasThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.get(); @@ -1070,6 +1072,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted} */ + @Experimental public boolean hasCompleted() { NotificationLite nl = ssm.nl; Object o = ssm.get(); @@ -1080,6 +1083,7 @@ public boolean hasCompleted() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ + @Experimental public Throwable getThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.get(); @@ -1092,12 +1096,14 @@ public Throwable getThrowable() { * Returns the current number of items (non-terminal events) available for replay. * @return the number of items available */ + @Experimental public int size() { return state.size(); } /** * @return true if the Subject holds at least one non-terminal event available for replay */ + @Experimental public boolean hasAnyValue() { return !state.isEmpty(); } @@ -1107,6 +1113,7 @@ public boolean hasAnyValue() { * @return returns a snapshot of the currently buffered non-terminal events. */ @SuppressWarnings("unchecked") + @Experimental public Object[] getValues() { return state.toArray((T[])EMPTY_ARRAY); } @@ -1116,6 +1123,7 @@ public Object[] getValues() { * @param a the array to fill in * @return the array {@code a} if it had enough capacity or a new array containing the available values */ + @Experimental public T[] getValues(T[] a) { return state.toArray(a); } From db364ba3d4b7fa5907a3ce3a7f94104d0cfde35c Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 21 Jan 2015 12:12:19 +0100 Subject: [PATCH 068/857] MergeTest.testConcurrency timeout to let other tests run --- src/test/java/rx/internal/operators/OperatorMergeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 48e7680581..46a5543a8b 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -494,7 +494,7 @@ public void call() { }); } - @Test + @Test(timeout = 10000) public void testConcurrency() { Observable o = Observable.range(1, 10000).subscribeOn(Schedulers.newThread()); From 67e8cfede63cde014e967455133852f4ac5dcb9f Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 21 Jan 2015 13:13:46 +0100 Subject: [PATCH 069/857] SynchronousQueue.clone fix --- src/main/java/rx/internal/util/SynchronizedQueue.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/util/SynchronizedQueue.java b/src/main/java/rx/internal/util/SynchronizedQueue.java index cf04bc021f..9fe867d93a 100644 --- a/src/main/java/rx/internal/util/SynchronizedQueue.java +++ b/src/main/java/rx/internal/util/SynchronizedQueue.java @@ -138,7 +138,9 @@ public synchronized boolean offer(T e) { @Override public synchronized Object clone() { - return list.clone(); + SynchronizedQueue q = new SynchronizedQueue(size); + q.addAll(list); + return q; } @Override From 46345351a19bfef441e8d1e4ca103dfe74aa5a57 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 21 Jan 2015 13:34:52 +0100 Subject: [PATCH 070/857] Fixed off-by-one error and value-drop in the window operator. --- .../operators/OperatorWindowWithTime.java | 24 ++++----- .../operators/OperatorWindowWithTimeTest.java | 49 ++++++++++++------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithTime.java b/src/main/java/rx/internal/operators/OperatorWindowWithTime.java index e875ce75d6..dd80a06a38 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithTime.java @@ -33,8 +33,8 @@ /** * Creates windows of values into the source sequence with timed window creation, length and size bounds. - * If timespan == timeshift, windows are non-overlapping but may not be continuous if size number of items were already - * emitted. If more items arrive after the window has reached its size bound, those items are dropped. + * If timespan == timeshift, windows are non-overlapping but always continuous, i.e., when the size bound is reached, a new + * window is opened. * *

Note that this conforms the Rx.NET behavior, but does not match former RxJava * behavior, which operated as a regular buffer and mapped its lists to Observables.

@@ -205,17 +205,17 @@ void replaceSubject() { } void emitValue(T t) { State s = state; - - if (s.consumer != null) { - s.consumer.onNext(t); - if (s.count == size) { - s.consumer.onCompleted(); - s = s.clear(); - } else { - s = s.next(); - } + if (s.consumer == null) { + replaceSubject(); + s = state; + } + s.consumer.onNext(t); + if (s.count == size - 1) { + s.consumer.onCompleted(); + s = s.clear(); + } else { + s = s.next(); } - state = s; } diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java index c61b4d24a1..22e6906463 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java @@ -15,22 +15,17 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; +import rx.*; import rx.Observable; import rx.Observer; -import rx.Scheduler; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Action1; +import rx.functions.*; import rx.schedulers.TestScheduler; public class OperatorWindowWithTimeTest { @@ -132,14 +127,14 @@ public void call() { }, delay, TimeUnit.MILLISECONDS); } - private Action1> observeWindow(final List list, final List> lists) { - return new Action1>() { + private Action1> observeWindow(final List list, final List> lists) { + return new Action1>() { @Override - public void call(Observable stringObservable) { - stringObservable.subscribe(new Observer() { + public void call(Observable stringObservable) { + stringObservable.subscribe(new Observer() { @Override public void onCompleted() { - lists.add(new ArrayList(list)); + lists.add(new ArrayList(list)); list.clear(); } @@ -149,11 +144,31 @@ public void onError(Throwable e) { } @Override - public void onNext(String args) { + public void onNext(T args) { list.add(args); } }); } }; } -} \ No newline at end of file + @Test + public void testExactWindowSize() { + Observable> source = Observable.range(1, 10).window(1, TimeUnit.MINUTES, 3, scheduler); + + final List list = new ArrayList(); + final List> lists = new ArrayList>(); + + source.subscribe(observeWindow(list, lists)); + + assertEquals(4, lists.size()); + assertEquals(3, lists.get(0).size()); + assertEquals(Arrays.asList(1, 2, 3), lists.get(0)); + assertEquals(3, lists.get(1).size()); + assertEquals(Arrays.asList(4, 5, 6), lists.get(1)); + assertEquals(3, lists.get(2).size()); + assertEquals(Arrays.asList(7, 8, 9), lists.get(2)); + assertEquals(1, lists.get(3).size()); + assertEquals(Arrays.asList(10), lists.get(3)); + } + +} From f410739c423dfdcb694d7573ccf7755706736094 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 21 Jan 2015 14:48:08 +0100 Subject: [PATCH 071/857] BackpressureTests: adjusted some tests to make them more reliable. --- src/test/java/rx/BackpressureTests.java | 63 +++++++++++++++++++------ 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index 3cabe3e7b3..087ada7887 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; @@ -135,8 +136,9 @@ public void testMergeAsyncThenObserveOn() { // 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 - assertTrue(c1.get() < RxRingBuffer.SIZE * 5); - assertTrue(c2.get() < RxRingBuffer.SIZE * 5); + // 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); } @Test @@ -409,18 +411,49 @@ public void testFirehoseFailsAsExpected() { assertTrue(ts.getOnErrorEvents().get(0) instanceof MissingBackpressureException); } - @Test(timeout = 2000) + @Test(timeout = 10000) public void testOnBackpressureDrop() { - int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow - AtomicInteger c = new AtomicInteger(); - TestSubscriber ts = new TestSubscriber(); - firehose(c).onBackpressureDrop().observeOn(Schedulers.computation()).map(SLOW_PASS_THRU).take(NUM).subscribe(ts); - ts.awaitTerminalEvent(); - ts.assertNoErrors(); - System.out.println("testOnBackpressureDrop => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c.get() + " Last value: " + ts.getOnNextEvents().get(NUM - 1)); - assertEquals(NUM, ts.getOnNextEvents().size()); - // it drop, so we should get some number far higher than what would have sequentially incremented - assertTrue(NUM < ts.getOnNextEvents().get(NUM - 1).intValue()); + for (int i = 0; i < 100; i++) { + int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow + AtomicInteger c = new AtomicInteger(); + TestSubscriber ts = new TestSubscriber(); + firehose(c).onBackpressureDrop() + .observeOn(Schedulers.computation()) + .map(SLOW_PASS_THRU).take(NUM).subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + + + List onNextEvents = ts.getOnNextEvents(); + assertEquals(NUM, onNextEvents.size()); + + Integer lastEvent = onNextEvents.get(NUM - 1); + + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(NUM - 1 <= lastEvent.intValue()); + } + } + @Test(timeout = 10000) + public void testOnBackpressureDropSynchronous() { + for (int i = 0; i < 100; i++) { + int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow + AtomicInteger c = new AtomicInteger(); + TestSubscriber ts = new TestSubscriber(); + firehose(c).onBackpressureDrop() + .map(SLOW_PASS_THRU).take(NUM).subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + + List onNextEvents = ts.getOnNextEvents(); + assertEquals(NUM, onNextEvents.size()); + + Integer lastEvent = onNextEvents.get(NUM - 1); + + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(NUM - 1 <= lastEvent.intValue()); + } } @Test(timeout = 2000) @@ -521,8 +554,8 @@ public void call(final Subscriber s) { public Integer call(Integer t1) { // be slow ... but faster than Thread.sleep(1) String t = ""; - for (int i = 0; i < 10000; i++) { - t = String.valueOf(i); + for (int i = 1000; i >= 0; i--) { + t = String.valueOf(i + t.hashCode()); } return t1; } From 5ee1e14493aba4248e6eb7e078864bc9a5b77ab0 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 21 Jan 2015 14:54:48 +0100 Subject: [PATCH 072/857] Fixed SLOW_PASS_THRU to prevent JIT optimizing it away. --- src/test/java/rx/BackpressureTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index 087ada7887..0760350f67 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -549,14 +549,16 @@ public void call(final Subscriber s) { } final static Func1 SLOW_PASS_THRU = new Func1() { - + volatile int sink; @Override public Integer call(Integer t1) { // be slow ... but faster than Thread.sleep(1) String t = ""; + int s = sink; for (int i = 1000; i >= 0; i--) { - t = String.valueOf(i + t.hashCode()); + t = String.valueOf(i + t.hashCode() + s); } + sink = t.hashCode(); return t1; } From de6a9d3cc48a517524dcbe24de23130f37ff2888 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 21 Jan 2015 15:21:08 +0100 Subject: [PATCH 073/857] RefCountAsync: adjusted time values as 1 ms is unreliable --- .../java/rx/internal/operators/OnSubscribeRefCountTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java index bdd6f47e3e..c70fa6dbf1 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java @@ -62,7 +62,7 @@ public void setUp() { public void testRefCountAsync() { final AtomicInteger subscribeCount = new AtomicInteger(); final AtomicInteger nextCount = new AtomicInteger(); - Observable r = Observable.timer(0, 1, TimeUnit.MILLISECONDS) + Observable r = Observable.timer(0, 5, TimeUnit.MILLISECONDS) .doOnSubscribe(new Action0() { @Override @@ -94,7 +94,7 @@ public void call(Long l) { // give time to emit try { - Thread.sleep(50); + Thread.sleep(52); } catch (InterruptedException e) { } From 6da4b03afbe5e621eddf1dd8ad867a0c967be0ed Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Wed, 21 Jan 2015 09:10:22 -0800 Subject: [PATCH 074/857] Use a PBQ instead of a PQ on the trampoline. Moves counter inside to worker class and ditches the separate field updater. --- .../rx/schedulers/TrampolineScheduler.java | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index 150fbf5ef9..e7ad0d8b45 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -15,7 +15,7 @@ */ package rx.schedulers; -import java.util.PriorityQueue; +import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -45,14 +45,12 @@ public Worker createWorker() { /* package accessible for unit tests */TrampolineScheduler() { } - volatile int counter; - static final AtomicIntegerFieldUpdater COUNTER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(TrampolineScheduler.class, "counter"); - private class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { - final PriorityQueue queue = new PriorityQueue(); + private final PriorityBlockingQueue queue = new PriorityBlockingQueue(); private final BooleanSubscription innerSubscription = new BooleanSubscription(); private final AtomicInteger wip = new AtomicInteger(); + private final AtomicInteger counter = new AtomicInteger(); @Override public Subscription schedule(Action0 action) { @@ -70,18 +68,15 @@ private Subscription enqueue(Action0 action, long execTime) { if (innerSubscription.isUnsubscribed()) { return Subscriptions.unsubscribed(); } - final TimedAction timedAction = new TimedAction(action, execTime, COUNTER_UPDATER.incrementAndGet(TrampolineScheduler.this)); - synchronized (queue) { - queue.add(timedAction); - } + final TimedAction timedAction = new TimedAction(action, execTime, counter.incrementAndGet()); + queue.add(timedAction); if (wip.getAndIncrement() == 0) { do { - TimedAction polled; - synchronized (queue) { - polled = queue.poll(); + final TimedAction polled = queue.poll(); + if (polled != null) { + polled.action.call(); } - polled.action.call(); } while (wip.decrementAndGet() > 0); return Subscriptions.unsubscribed(); } else { @@ -90,9 +85,7 @@ private Subscription enqueue(Action0 action, long execTime) { @Override public void call() { - synchronized (queue) { - queue.remove(timedAction); - } + queue.remove(timedAction); } }); From ab8460bfb692cb2c0e7d6e6e34522e641212f42d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 22 Jan 2015 12:13:06 +0100 Subject: [PATCH 075/857] Merge: fixed hangs & missed scalar emissions --- .../rx/internal/operators/OperatorMerge.java | 79 ++++++++++--------- .../internal/operators/OperatorMergeTest.java | 21 ++++- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 269135446a..9b0eb074aa 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -17,20 +17,13 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.*; -import rx.Observable; +import rx.*; import rx.Observable.Operator; -import rx.Producer; -import rx.Subscriber; -import rx.exceptions.CompositeException; -import rx.exceptions.MissingBackpressureException; -import rx.exceptions.OnErrorThrowable; +import rx.exceptions.*; import rx.functions.Func1; -import rx.internal.util.RxRingBuffer; -import rx.internal.util.ScalarSynchronousObservable; -import rx.internal.util.SubscriptionIndexedRingBuffer; +import rx.internal.util.*; /** * Flattens a list of {@link Observable}s into one {@code Observable}, without any transformation. @@ -135,7 +128,7 @@ private static final class MergeSubscriber extends Subscriber> childrenSubscribers; - private RxRingBuffer scalarValueQueue = null; + private volatile RxRingBuffer scalarValueQueue = null; /* protected by lock on MergeSubscriber instance */ private int missedEmitting = 0; @@ -266,9 +259,8 @@ private void handleScalarSynchronousObservableWithoutRequestLimits(ScalarSynchro request(1); return; } else { - initScalarValueQueueIfNeeded(); try { - scalarValueQueue.onNext(value); + getOrCreateScalarValueQueue().onNext(value); } catch (MissingBackpressureException e) { onError(e); } @@ -306,19 +298,20 @@ private void handleScalarSynchronousObservableWithRequestLimits(ScalarSynchronou // if we didn't return above we need to enqueue // enqueue the values for later delivery - initScalarValueQueueIfNeeded(); try { - scalarValueQueue.onNext(t.get()); + getOrCreateScalarValueQueue().onNext(t.get()); } catch (MissingBackpressureException e) { onError(e); } } - private void initScalarValueQueueIfNeeded() { - if (scalarValueQueue == null) { - scalarValueQueue = RxRingBuffer.getSpmcInstance(); - add(scalarValueQueue); + private RxRingBuffer getOrCreateScalarValueQueue() { + RxRingBuffer svq = scalarValueQueue; + if (svq == null) { + svq = RxRingBuffer.getSpmcInstance(); + scalarValueQueue = svq; } + return svq; } private synchronized boolean releaseEmitLock() { @@ -381,13 +374,14 @@ private void drainChildrenQueues() { * ONLY call when holding the EmitLock. */ private int drainScalarValueQueue() { - if (scalarValueQueue != null) { + RxRingBuffer svq = scalarValueQueue; + if (svq != null) { long r = mergeProducer.requested; int emittedWhileDraining = 0; if (r < 0) { // drain it all Object o = null; - while ((o = scalarValueQueue.poll()) != null) { + while ((o = svq.poll()) != null) { on.accept(actual, o); emittedWhileDraining++; } @@ -395,7 +389,7 @@ private int drainScalarValueQueue() { // drain what was requested long toEmit = r; for (int i = 0; i < toEmit; i++) { - Object o = scalarValueQueue.poll(); + Object o = svq.poll(); if (o == null) { break; } else { @@ -469,7 +463,7 @@ public void onCompleted() { boolean c = false; synchronized (this) { completed = true; - if (wip == 0 && (scalarValueQueue == null || scalarValueQueue.isEmpty())) { + if (wip == 0) { c = true; } } @@ -494,25 +488,38 @@ void completeInner(InnerSubscriber s) { } private void drainAndComplete() { - drainQueuesIfNeeded(); // TODO need to confirm whether this is needed or not - if (delayErrors) { - Queue es = null; + boolean moreToDrain = true; + while (moreToDrain) { synchronized (this) { - es = exceptions; + missedEmitting = 0; } - if (es != null) { - if (es.isEmpty()) { - actual.onCompleted(); - } else if (es.size() == 1) { - actual.onError(es.poll()); + drainScalarValueQueue(); + drainChildrenQueues(); + synchronized (this) { + moreToDrain = missedEmitting > 0; + } + } + RxRingBuffer svq = scalarValueQueue; + if (svq == null || svq.isEmpty()) { + if (delayErrors) { + Queue es = null; + synchronized (this) { + es = exceptions; + } + if (es != null) { + if (es.isEmpty()) { + actual.onCompleted(); + } else if (es.size() == 1) { + actual.onError(es.poll()); + } else { + actual.onError(new CompositeException(es)); + } } else { - actual.onError(new CompositeException(es)); + actual.onCompleted(); } } else { actual.onCompleted(); } - } else { - actual.onCompleted(); } } diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 46a5543a8b..fa861f68ea 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -628,6 +628,16 @@ public void onNext(Integer t) { assertTrue(generated1.get() >= RxRingBuffer.SIZE * 2 && generated1.get() <= RxRingBuffer.SIZE * 4); } + @Test + public void testBackpressureUpstream2InLoop() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + System.err.flush(); + System.out.println("---"); + System.out.flush(); + testBackpressureUpstream2(); + } + } + @Test public void testBackpressureUpstream2() throws InterruptedException { final AtomicInteger generated1 = new AtomicInteger(); @@ -636,21 +646,24 @@ public void testBackpressureUpstream2() throws InterruptedException { TestSubscriber testSubscriber = new TestSubscriber() { @Override public void onNext(Integer t) { - System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); super.onNext(t); } }; Observable.merge(o1.take(RxRingBuffer.SIZE * 2), Observable.just(-99)).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); + + List onNextEvents = testSubscriber.getOnNextEvents(); + + System.out.println("Generated 1: " + generated1.get() + " / received: " + onNextEvents.size()); + System.out.println(onNextEvents); + if (testSubscriber.getOnErrorEvents().size() > 0) { testSubscriber.getOnErrorEvents().get(0).printStackTrace(); } testSubscriber.assertNoErrors(); - System.err.println(testSubscriber.getOnNextEvents()); - assertEquals(RxRingBuffer.SIZE * 2 + 1, testSubscriber.getOnNextEvents().size()); + assertEquals(RxRingBuffer.SIZE * 2 + 1, onNextEvents.size()); // it should be between the take num and requested batch size across the async boundary - System.out.println("Generated 1: " + generated1.get()); assertTrue(generated1.get() >= RxRingBuffer.SIZE * 2 && generated1.get() <= RxRingBuffer.SIZE * 3); } From 51113b7ca3cadaa1bcb52f75d6014b58049ffeb5 Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Thu, 22 Jan 2015 14:28:31 -0800 Subject: [PATCH 076/857] Fixes indent. --- src/main/java/rx/schedulers/TrampolineScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index e7ad0d8b45..78e73987f8 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -75,7 +75,7 @@ private Subscription enqueue(Action0 action, long execTime) { do { final TimedAction polled = queue.poll(); if (polled != null) { - polled.action.call(); + polled.action.call(); } } while (wip.decrementAndGet() > 0); return Subscriptions.unsubscribed(); From dad2a8c250383847f7c5533b57c7341ed602f80d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 23 Jan 2015 09:34:44 +0100 Subject: [PATCH 077/857] Operator DoTakeWhile --- src/main/java/rx/Observable.java | 20 +++ .../operators/OperatorDoTakeWhile.java | 83 +++++++++++++ .../operators/OperatorDoTakeWhileTest.java | 117 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/main/java/rx/internal/operators/OperatorDoTakeWhile.java create mode 100644 src/test/java/rx/internal/operators/OperatorDoTakeWhileTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 65c8c3a3a2..ee254b3b5c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7824,6 +7824,26 @@ public final Observable takeWhile(final Func1 predicate) return lift(new OperatorTakeWhile(predicate)); } + /** + * Returns an Observable that first emits items emitted by the source Observable, + * checks the specified condition after each item, and + * then completes as soon as this condition is not satisfied. + *

+ * The difference between this operator and {@link #takeWhile(Func1)} is that here, the condition is evaluated after + * the item was emitted. + * + * @param predicate + * a function that evaluates an item emitted by the source Observable and returns a Boolean + * @return an Observable that first emits items emitted by the source Observable, + * checks the specified condition after each item, and + * then completes as soon as this condition is not satisfied. + * @see Observable#takeWhile(Func1) + */ + @Experimental + public final Observable doTakeWhile(final Func1 predicate) { + return lift(new OperatorDoTakeWhile(predicate)); + } + /** * Returns an Observable that emits only the first item emitted by the source Observable during sequential * time windows of a specified duration. diff --git a/src/main/java/rx/internal/operators/OperatorDoTakeWhile.java b/src/main/java/rx/internal/operators/OperatorDoTakeWhile.java new file mode 100644 index 0000000000..388d4ea217 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorDoTakeWhile.java @@ -0,0 +1,83 @@ +/** + * 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.Operator; +import rx.Subscriber; +import rx.annotations.Experimental; +import rx.functions.Func1; + +/** + * Returns an Observable that emits items emitted by the source Observable until + * the provided predicate returns false + *

+ */ +@Experimental +public final class OperatorDoTakeWhile implements Operator { + + private final Func1 predicate; + + public OperatorDoTakeWhile(final Func1 predicate) { + this.predicate = predicate; + } + + @Override + public Subscriber call(final Subscriber child) { + Subscriber parent = new Subscriber() { + + private boolean done = false; + + @Override + public void onNext(T args) { + child.onNext(args); + + boolean doContinue = false; + try { + doContinue = predicate.call(args); + } catch (Throwable e) { + done = true; + child.onError(e); + unsubscribe(); + return; + } + if (!doContinue) { + done = true; + child.onCompleted(); + unsubscribe(); + } + } + + @Override + public void onCompleted() { + if (!done) { + child.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + if (!done) { + child.onError(e); + } + } + + }; + child.add(parent); // don't unsubscribe downstream + + return parent; + } + +} diff --git a/src/test/java/rx/internal/operators/OperatorDoTakeWhileTest.java b/src/test/java/rx/internal/operators/OperatorDoTakeWhileTest.java new file mode 100644 index 0000000000..09e717f658 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorDoTakeWhileTest.java @@ -0,0 +1,117 @@ +/** + * 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.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import rx.*; +import rx.exceptions.TestException; +import rx.functions.Func1; +import rx.internal.util.UtilityFunctions; +; + +public class OperatorDoTakeWhileTest { + @Test + public void takeEmpty() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.empty().doTakeWhile(UtilityFunctions.alwaysTrue()).subscribe(o); + + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onCompleted(); + } + @Test + public void takeAll() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.just(1, 2).doTakeWhile(UtilityFunctions.alwaysTrue()).subscribe(o); + + verify(o).onNext(1); + verify(o).onNext(2); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onCompleted(); + } + @Test + public void takeFirst() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.just(1, 2).doTakeWhile(UtilityFunctions.alwaysFalse()).subscribe(o); + + verify(o).onNext(1); + verify(o, never()).onNext(2); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onCompleted(); + } + @Test + public void takeSome() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.just(1, 2, 3).doTakeWhile(new Func1() { + @Override + public Boolean call(Integer t1) { + return t1 < 2; + } + }).subscribe(o); + + verify(o).onNext(1); + verify(o).onNext(2); + verify(o, never()).onNext(3); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onCompleted(); + } + @Test + public void functionThrows() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.just(1, 2, 3).doTakeWhile(new Func1() { + @Override + public Boolean call(Integer t1) { + throw new TestException("Forced failure"); + } + }).subscribe(o); + + verify(o).onNext(1); + verify(o, never()).onNext(2); + verify(o, never()).onNext(3); + verify(o).onError(any(TestException.class)); + verify(o, never()).onCompleted(); + } + @Test + public void sourceThrows() { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.just(1) + .concatWith(Observable.error(new TestException())) + .concatWith(Observable.just(2)) + .doTakeWhile(UtilityFunctions.alwaysTrue()).subscribe(o); + + verify(o).onNext(1); + verify(o, never()).onNext(2); + verify(o).onError(any(TestException.class)); + verify(o, never()).onCompleted(); + } +} From 63da8b19da954bc8262087cf5607276a5afcde28 Mon Sep 17 00:00:00 2001 From: Jason Neufeld Date: Fri, 23 Jan 2015 09:33:43 -0800 Subject: [PATCH 078/857] Back to a field updater. --- src/main/java/rx/schedulers/TrampolineScheduler.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index 78e73987f8..feb0d93ee4 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -45,12 +45,13 @@ public Worker createWorker() { /* package accessible for unit tests */TrampolineScheduler() { } - private class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { + private static class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { + private static final AtomicIntegerFieldUpdater COUNTER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(InnerCurrentThreadScheduler.class, "counter"); + volatile int counter; private final PriorityBlockingQueue queue = new PriorityBlockingQueue(); private final BooleanSubscription innerSubscription = new BooleanSubscription(); private final AtomicInteger wip = new AtomicInteger(); - private final AtomicInteger counter = new AtomicInteger(); @Override public Subscription schedule(Action0 action) { @@ -68,7 +69,7 @@ private Subscription enqueue(Action0 action, long execTime) { if (innerSubscription.isUnsubscribed()) { return Subscriptions.unsubscribed(); } - final TimedAction timedAction = new TimedAction(action, execTime, counter.incrementAndGet()); + final TimedAction timedAction = new TimedAction(action, execTime, COUNTER_UPDATER.incrementAndGet(this)); queue.add(timedAction); if (wip.getAndIncrement() == 0) { From 1ff313fd3964355d49d53ffa403be7ad6a41a603 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 26 Jan 2015 13:26:50 +0100 Subject: [PATCH 079/857] Merge with max concurrency now supports backpressure. --- .../operators/OperatorMergeMaxConcurrent.java | 288 +++++++++++++++--- .../OperatorMergeMaxConcurrentTest.java | 157 +++++++++- 2 files changed, 393 insertions(+), 52 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java b/src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java index d75425bb6d..9f28f3199e 100644 --- a/src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java +++ b/src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java @@ -15,12 +15,14 @@ */ package rx.internal.operators; -import java.util.LinkedList; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import rx.Observable; +import java.util.*; +import java.util.concurrent.atomic.*; + +import rx.*; import rx.Observable.Operator; -import rx.Subscriber; +import rx.Observable; +import rx.exceptions.MissingBackpressureException; +import rx.internal.util.RxRingBuffer; import rx.observers.SerializedSubscriber; import rx.subscriptions.CompositeSubscription; @@ -47,9 +49,24 @@ public Subscriber> call(Subscriber ch final CompositeSubscription csub = new CompositeSubscription(); child.add(csub); - return new SourceSubscriber(maxConcurrency, s, csub); + SourceSubscriber ssub = new SourceSubscriber(maxConcurrency, s, csub); + child.setProducer(new MergeMaxConcurrentProducer(ssub)); + + return ssub; + } + /** Routes the requests from downstream to the sourcesubscriber. */ + static final class MergeMaxConcurrentProducer implements Producer { + final SourceSubscriber ssub; + public MergeMaxConcurrentProducer(SourceSubscriber ssub) { + this.ssub = ssub; + } + @Override + public void request(long n) { + ssub.downstreamRequest(n); + } } static final class SourceSubscriber extends Subscriber> { + final NotificationLite nl = NotificationLite.instance(); final int maxConcurrency; final Subscriber s; final CompositeSubscription csub; @@ -57,24 +74,50 @@ static final class SourceSubscriber extends Subscriber WIP_UPDATER + static final AtomicIntegerFieldUpdater WIP = AtomicIntegerFieldUpdater.newUpdater(SourceSubscriber.class, "wip"); + volatile int sourceIndex; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater SOURCE_INDEX + = AtomicIntegerFieldUpdater.newUpdater(SourceSubscriber.class, "sourceIndex"); /** Guarded by guard. */ int active; /** Guarded by guard. */ final Queue> queue; + /** Indicates the emitting phase. Guarded by this. */ + boolean emitting; + /** Counts the missed emitting calls. Guarded by this. */ + int missedEmitting; + /** The last buffer index in the round-robin drain scheme. Accessed while emitting == true. */ + int lastIndex; + + /** Guarded by itself. */ + final List subscribers; + + volatile long requested; + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED + = AtomicLongFieldUpdater.newUpdater(SourceSubscriber.class, "requested"); + + public SourceSubscriber(int maxConcurrency, Subscriber s, CompositeSubscription csub) { super(s); this.maxConcurrency = maxConcurrency; this.s = s; this.csub = csub; this.guard = new Object(); - this.queue = new LinkedList>(); + this.queue = new ArrayDeque>(maxConcurrency); + this.subscribers = Collections.synchronizedList(new ArrayList()); this.wip = 1; } + @Override + public void onStart() { + request(maxConcurrency); + } + @Override public void onNext(Observable t) { synchronized (guard) { @@ -94,50 +137,213 @@ void subscribeNext() { queue.poll(); } - Subscriber itemSub = new Subscriber() { - boolean once = true; - @Override - public void onNext(T t) { - s.onNext(t); - } - - @Override - public void onError(Throwable e) { - SourceSubscriber.this.onError(e); - } - - @Override - public void onCompleted() { - if (once) { - once = false; - synchronized (guard) { - active--; - } - csub.remove(this); - - subscribeNext(); - - SourceSubscriber.this.onCompleted(); - } - } - - }; + MergeItemSubscriber itemSub = new MergeItemSubscriber(SOURCE_INDEX.getAndIncrement(this)); + subscribers.add(itemSub); + csub.add(itemSub); - WIP_UPDATER.incrementAndGet(this); + + WIP.incrementAndGet(this); t.unsafeSubscribe(itemSub); + + request(1); } @Override public void onError(Throwable e) { - s.onError(e); - unsubscribe(); + Object[] active; + synchronized (subscribers) { + active = subscribers.toArray(); + subscribers.clear(); + } + + try { + s.onError(e); + + unsubscribe(); + } finally { + for (Object o : active) { + @SuppressWarnings("unchecked") + MergeItemSubscriber a = (MergeItemSubscriber)o; + a.release(); + } + } + } @Override public void onCompleted() { - if (WIP_UPDATER.decrementAndGet(this) == 0) { - s.onCompleted(); + WIP.decrementAndGet(this); + drain(); + } + + protected void downstreamRequest(long n) { + for (;;) { + long r = requested; + long u; + if (r != Long.MAX_VALUE && n == Long.MAX_VALUE) { + u = Long.MAX_VALUE; + } else + if (r + n < 0) { + u = Long.MAX_VALUE; + } else { + u = r + n; + } + if (REQUESTED.compareAndSet(this, r, u)) { + break; + } + } + drain(); + } + + protected void drain() { + synchronized (this) { + if (emitting) { + missedEmitting++; + return; + } + emitting = true; + missedEmitting = 0; + } + final List.MergeItemSubscriber> subs = subscribers; + final Subscriber child = s; + Object[] active = new Object[subs.size()]; + do { + long r; + + outer: + while ((r = requested) > 0) { + int idx = lastIndex; + synchronized (subs) { + if (subs.size() == active.length) { + active = subs.toArray(active); + } else { + active = subs.toArray(); + } + } + + int resumeIndex = 0; + int j = 0; + for (Object o : active) { + @SuppressWarnings("unchecked") + MergeItemSubscriber e = (MergeItemSubscriber)o; + if (e.index == idx) { + resumeIndex = j; + break; + } + j++; + } + int sumConsumed = 0; + for (int i = 0; i < active.length; i++) { + j = (i + resumeIndex) % active.length; + + @SuppressWarnings("unchecked") + final MergeItemSubscriber e = (MergeItemSubscriber)active[j]; + final RxRingBuffer b = e.buffer; + lastIndex = e.index; + + if (!e.once && b.peek() == null) { + subs.remove(e); + + synchronized (guard) { + this.active--; + } + csub.remove(e); + + e.release(); + + subscribeNext(); + + WIP.decrementAndGet(this); + + continue outer; + } + + int consumed = 0; + Object v; + while (r > 0 && (v = b.poll()) != null) { + nl.accept(child, v); + if (child.isUnsubscribed()) { + return; + } + r--; + consumed++; + } + if (consumed > 0) { + sumConsumed += consumed; + REQUESTED.addAndGet(this, -consumed); + e.requestMore(consumed); + } + if (r == 0) { + break outer; + } + } + if (sumConsumed == 0) { + break; + } + } + + if (active.length == 0) { + if (wip == 0) { + child.onCompleted(); + return; + } + } + synchronized (this) { + if (missedEmitting == 0) { + emitting = false; + break; + } + missedEmitting = 0; + } + } while (true); + } + final class MergeItemSubscriber extends Subscriber { + volatile boolean once = true; + final int index; + final RxRingBuffer buffer; + + public MergeItemSubscriber(int index) { + buffer = RxRingBuffer.getSpmcInstance(); + this.index = index; + } + + @Override + public void onStart() { + request(RxRingBuffer.SIZE); + } + + @Override + public void onNext(T t) { + try { + buffer.onNext(t); + } catch (MissingBackpressureException ex) { + onError(ex); + return; + } + + drain(); + } + + @Override + public void onError(Throwable e) { + SourceSubscriber.this.onError(e); + } + + @Override + public void onCompleted() { + if (once) { + once = false; + drain(); + } + } + /** Request more from upstream. */ + void requestMore(long n) { + request(n); + } + void release() { + // NO-OP for now + buffer.release(); } } } diff --git a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java index be9a04cb7a..238526a3c3 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java @@ -15,23 +15,19 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; +import static org.junit.Assert.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; +import java.util.*; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.junit.*; +import org.mockito.*; +import rx.*; import rx.Observable; import rx.Observer; -import rx.Subscriber; +import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; public class OperatorMergeMaxConcurrentTest { @@ -157,4 +153,143 @@ public void testMergeALotOfSourcesOneByOneSynchronouslyTakeHalf() { } assertEquals(j, n / 2); } + + @Test + public void testSimple() { + for (int i = 1; i < 100; i++) { + TestSubscriber ts = new TestSubscriber(); + List> sourceList = new ArrayList>(i); + List result = new ArrayList(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j)); + result.add(j); + } + + Observable.merge(sourceList, i).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(result); + } + } + @Test + public void testSimpleOneLess() { + for (int i = 2; i < 100; i++) { + TestSubscriber ts = new TestSubscriber(); + List> sourceList = new ArrayList>(i); + List result = new ArrayList(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j)); + result.add(j); + } + + Observable.merge(sourceList, i - 1).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(result); + } + } + @Test(timeout = 10000) + public void testSympleAsyncLoop() { + for (int i = 0; i < 200; i++) { + testSimpleAsync(); + } + } + @Test(timeout = 10000) + public void testSimpleAsync() { + for (int i = 1; i < 100; i++) { + TestSubscriber ts = new TestSubscriber(); + List> sourceList = new ArrayList>(i); + Set expected = new HashSet(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j).subscribeOn(Schedulers.io())); + expected.add(j); + } + + Observable.merge(sourceList, i).subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + Set actual = new HashSet(ts.getOnNextEvents()); + + assertEquals(expected, actual); + } + } + @Test(timeout = 10000) + public void testSimpleOneLessAsyncLoop() { + for (int i = 0; i < 200; i++) { + testSimpleOneLessAsync(); + } + } + @Test(timeout = 10000) + public void testSimpleOneLessAsync() { + for (int i = 2; i < 100; i++) { + TestSubscriber ts = new TestSubscriber(); + List> sourceList = new ArrayList>(i); + Set expected = new HashSet(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j).subscribeOn(Schedulers.io())); + expected.add(j); + } + + Observable.merge(sourceList, i - 1).subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + Set actual = new HashSet(ts.getOnNextEvents()); + + assertEquals(expected, actual); + } + } + @Test(timeout = 5000) + public void testBackpressureHonored() throws Exception { + List> sourceList = new ArrayList>(3); + + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + + final CountDownLatch cdl = new CountDownLatch(5); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(0); + } + @Override + public void onNext(Integer t) { + super.onNext(t); + cdl.countDown(); + } + }; + + Observable.merge(sourceList, 2).subscribe(ts); + + ts.requestMore(5); + + cdl.await(); + + ts.assertNoErrors(); + assertEquals(5, ts.getOnNextEvents().size()); + assertEquals(0, ts.getOnCompletedEvents().size()); + + ts.unsubscribe(); + } + @Test(timeout = 5000) + public void testTake() throws Exception { + List> sourceList = new ArrayList>(3); + + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + + TestSubscriber ts = new TestSubscriber(); + + Observable.merge(sourceList, 2).take(5).subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + assertEquals(5, ts.getOnNextEvents().size()); + } } From a3b454d849bb461e9ce09d967cfec31b1ebc0c8e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 26 Jan 2015 13:28:59 +0100 Subject: [PATCH 080/857] Fixed line delimiters. --- .../util/BackpressureDrainManager.java | 480 +++++++++--------- 1 file changed, 240 insertions(+), 240 deletions(-) diff --git a/src/main/java/rx/internal/util/BackpressureDrainManager.java b/src/main/java/rx/internal/util/BackpressureDrainManager.java index 7e521d0bba..f4a95573e7 100644 --- a/src/main/java/rx/internal/util/BackpressureDrainManager.java +++ b/src/main/java/rx/internal/util/BackpressureDrainManager.java @@ -1,240 +1,240 @@ -/** - * 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.util; - -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -import rx.Producer; -import rx.annotations.Experimental; - -/** - * Manages the producer-backpressure-consumer interplay by - * matching up available elements with requested elements and/or - * terminal events. - */ -@Experimental -public final class BackpressureDrainManager implements Producer { - /** - * Interface representing the minimal callbacks required - * to operate the drain part of a backpressure system. - */ - public interface BackpressureQueueCallback { - /** - * Override this method to peek for the next element, - * null meaning no next element available now. - *

It will be called plain and while holding this object's monitor. - * @return the next element or null if no next element available - */ - Object peek(); - /** - * Override this method to poll (consume) the next element, - * null meaning no next element available now. - * @return the next element or null if no next element available - */ - Object poll(); - /** - * Override this method to deliver an element to downstream. - * The logic ensures that this happens only in the right conditions. - * @param value the value to deliver, not null - * @return true indicates that one should terminate the emission loop unconditionally - * and not deliver any further elements or terminal events. - */ - boolean accept(Object value); - /** - * Override this method to deliver a normal or exceptional - * terminal event. - * @param exception if not null, contains the terminal exception - */ - void complete(Throwable exception); - } - - /** The request counter, updated via REQUESTED_COUNTER. */ - protected volatile long requestedCount; - /** Atomically updates the the requestedCount field. */ - protected static final AtomicLongFieldUpdater REQUESTED_COUNT - = AtomicLongFieldUpdater.newUpdater(BackpressureDrainManager.class, "requestedCount"); - /** Indicates if one is in emitting phase, guarded by this. */ - protected boolean emitting; - /** Indicates a terminal state. */ - protected volatile boolean terminated; - /** Indicates an error state, barrier is provided via terminated. */ - protected Throwable exception; - /** The callbacks to manage the drain. */ - protected final BackpressureQueueCallback actual; - /** - * Constructs a backpressure drain manager with 0 requesedCount, - * no terminal event and not emitting. - * @param actual he queue callback to check for new element availability - */ - public BackpressureDrainManager(BackpressureQueueCallback actual) { - this.actual = actual; - } - /** - * Checks if a terminal state has been reached. - * @return true if a terminal state has been reached - */ - public final boolean isTerminated() { - return terminated; - } - /** - * Move into a terminal state. - * Call drain() anytime after. - */ - public final void terminate() { - terminated = true; - } - /** - * Move into a terminal state with an exception. - * Call drain() anytime after. - *

Serialized access is expected with respect to - * element emission. - * @param error the exception to deliver - */ - public final void terminate(Throwable error) { - if (!terminated) { - exception = error; - terminated = true; - } - } - /** - * Move into a terminal state and drain. - */ - public final void terminateAndDrain() { - terminated = true; - drain(); - } - /** - * Move into a terminal state with an exception and drain. - *

Serialized access is expected with respect to - * element emission. - * @param error the exception to deliver - */ - public final void terminateAndDrain(Throwable error) { - if (!terminated) { - exception = error; - terminated = true; - drain(); - } - } - @Override - public final void request(long n) { - if (n == 0) { - return; - } - boolean mayDrain; - long r; - long u; - do { - r = requestedCount; - mayDrain = r == 0; - if (r == Long.MAX_VALUE) { - break; - } - if (n == Long.MAX_VALUE) { - u = n; - mayDrain = true; - } else { - if (r > Long.MAX_VALUE - n) { - u = Long.MAX_VALUE; - } else { - u = r + n; - } - } - } while (!REQUESTED_COUNT.compareAndSet(this, r, u)); - // since we implement producer, we have to call drain - // on a 0-n request transition - if (mayDrain) { - drain(); - } - } - /** - * Try to drain the "queued" elements and terminal events - * by considering the available and requested event counts. - */ - public final void drain() { - long n; - boolean term; - synchronized (this) { - if (emitting) { - return; - } - emitting = true; - term = terminated; - } - n = requestedCount; - boolean skipFinal = false; - try { - BackpressureQueueCallback a = actual; - while (true) { - int emitted = 0; - while (n > 0 || term) { - Object o; - if (term) { - o = a.peek(); - if (o == null) { - skipFinal = true; - Throwable e = exception; - a.complete(e); - return; - } - if (n == 0) { - break; - } - } - o = a.poll(); - if (o == null) { - break; - } else { - if (a.accept(o)) { - skipFinal = true; - return; - } - n--; - emitted++; - } - } - synchronized (this) { - term = terminated; - boolean more = a.peek() != null; - // if no backpressure below - if (requestedCount == Long.MAX_VALUE) { - // no new data arrived since the last poll - if (!more && !term) { - skipFinal = true; - emitting = false; - return; - } - n = Long.MAX_VALUE; - } else { - n = REQUESTED_COUNT.addAndGet(this, -emitted); - if ((n == 0 || !more) && (!term || more)) { - skipFinal = true; - emitting = false; - return; - } - } - } - } - } finally { - if (!skipFinal) { - synchronized (this) { - emitting = false; - } - } - } - - } -} +/** + * 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.util; + +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import rx.Producer; +import rx.annotations.Experimental; + +/** + * Manages the producer-backpressure-consumer interplay by + * matching up available elements with requested elements and/or + * terminal events. + */ +@Experimental +public final class BackpressureDrainManager implements Producer { + /** + * Interface representing the minimal callbacks required + * to operate the drain part of a backpressure system. + */ + public interface BackpressureQueueCallback { + /** + * Override this method to peek for the next element, + * null meaning no next element available now. + *

It will be called plain and while holding this object's monitor. + * @return the next element or null if no next element available + */ + Object peek(); + /** + * Override this method to poll (consume) the next element, + * null meaning no next element available now. + * @return the next element or null if no next element available + */ + Object poll(); + /** + * Override this method to deliver an element to downstream. + * The logic ensures that this happens only in the right conditions. + * @param value the value to deliver, not null + * @return true indicates that one should terminate the emission loop unconditionally + * and not deliver any further elements or terminal events. + */ + boolean accept(Object value); + /** + * Override this method to deliver a normal or exceptional + * terminal event. + * @param exception if not null, contains the terminal exception + */ + void complete(Throwable exception); + } + + /** The request counter, updated via REQUESTED_COUNTER. */ + protected volatile long requestedCount; + /** Atomically updates the the requestedCount field. */ + protected static final AtomicLongFieldUpdater REQUESTED_COUNT + = AtomicLongFieldUpdater.newUpdater(BackpressureDrainManager.class, "requestedCount"); + /** Indicates if one is in emitting phase, guarded by this. */ + protected boolean emitting; + /** Indicates a terminal state. */ + protected volatile boolean terminated; + /** Indicates an error state, barrier is provided via terminated. */ + protected Throwable exception; + /** The callbacks to manage the drain. */ + protected final BackpressureQueueCallback actual; + /** + * Constructs a backpressure drain manager with 0 requesedCount, + * no terminal event and not emitting. + * @param actual he queue callback to check for new element availability + */ + public BackpressureDrainManager(BackpressureQueueCallback actual) { + this.actual = actual; + } + /** + * Checks if a terminal state has been reached. + * @return true if a terminal state has been reached + */ + public final boolean isTerminated() { + return terminated; + } + /** + * Move into a terminal state. + * Call drain() anytime after. + */ + public final void terminate() { + terminated = true; + } + /** + * Move into a terminal state with an exception. + * Call drain() anytime after. + *

Serialized access is expected with respect to + * element emission. + * @param error the exception to deliver + */ + public final void terminate(Throwable error) { + if (!terminated) { + exception = error; + terminated = true; + } + } + /** + * Move into a terminal state and drain. + */ + public final void terminateAndDrain() { + terminated = true; + drain(); + } + /** + * Move into a terminal state with an exception and drain. + *

Serialized access is expected with respect to + * element emission. + * @param error the exception to deliver + */ + public final void terminateAndDrain(Throwable error) { + if (!terminated) { + exception = error; + terminated = true; + drain(); + } + } + @Override + public final void request(long n) { + if (n == 0) { + return; + } + boolean mayDrain; + long r; + long u; + do { + r = requestedCount; + mayDrain = r == 0; + if (r == Long.MAX_VALUE) { + break; + } + if (n == Long.MAX_VALUE) { + u = n; + mayDrain = true; + } else { + if (r > Long.MAX_VALUE - n) { + u = Long.MAX_VALUE; + } else { + u = r + n; + } + } + } while (!REQUESTED_COUNT.compareAndSet(this, r, u)); + // since we implement producer, we have to call drain + // on a 0-n request transition + if (mayDrain) { + drain(); + } + } + /** + * Try to drain the "queued" elements and terminal events + * by considering the available and requested event counts. + */ + public final void drain() { + long n; + boolean term; + synchronized (this) { + if (emitting) { + return; + } + emitting = true; + term = terminated; + } + n = requestedCount; + boolean skipFinal = false; + try { + BackpressureQueueCallback a = actual; + while (true) { + int emitted = 0; + while (n > 0 || term) { + Object o; + if (term) { + o = a.peek(); + if (o == null) { + skipFinal = true; + Throwable e = exception; + a.complete(e); + return; + } + if (n == 0) { + break; + } + } + o = a.poll(); + if (o == null) { + break; + } else { + if (a.accept(o)) { + skipFinal = true; + return; + } + n--; + emitted++; + } + } + synchronized (this) { + term = terminated; + boolean more = a.peek() != null; + // if no backpressure below + if (requestedCount == Long.MAX_VALUE) { + // no new data arrived since the last poll + if (!more && !term) { + skipFinal = true; + emitting = false; + return; + } + n = Long.MAX_VALUE; + } else { + n = REQUESTED_COUNT.addAndGet(this, -emitted); + if ((n == 0 || !more) && (!term || more)) { + skipFinal = true; + emitting = false; + return; + } + } + } + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + + } +} From 34bf40aa7536e63db5a81302deb98d497b0240ef Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 26 Jan 2015 13:33:45 +0100 Subject: [PATCH 081/857] Added capacity increase on poll. --- .../rx/internal/operators/OperatorOnBackpressureBuffer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index 1517736dbd..d323122f8a 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -125,7 +125,11 @@ public Object peek() { } @Override public Object poll() { - return queue.poll(); + Object value = queue.poll(); + if (capacity != null) { + capacity.incrementAndGet(); + } + return value; } private boolean assertCapacity() { From a868569c44fd8f44b0be0826f0387d788714c636 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 26 Jan 2015 13:41:32 +0100 Subject: [PATCH 082/857] Less concurrent threads and more in-line timeout detection. --- .../operators/OperatorMergeMaxConcurrentTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java index 238526a3c3..52c7ee21f2 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java @@ -18,7 +18,7 @@ import static org.junit.Assert.*; import java.util.*; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import org.junit.*; @@ -191,14 +191,14 @@ public void testSimpleOneLess() { } } @Test(timeout = 10000) - public void testSympleAsyncLoop() { + public void testSimpleAsyncLoop() { for (int i = 0; i < 200; i++) { testSimpleAsync(); } } @Test(timeout = 10000) public void testSimpleAsync() { - for (int i = 1; i < 100; i++) { + for (int i = 1; i < 50; i++) { TestSubscriber ts = new TestSubscriber(); List> sourceList = new ArrayList>(i); Set expected = new HashSet(i); @@ -209,7 +209,7 @@ public void testSimpleAsync() { Observable.merge(sourceList, i).subscribe(ts); - ts.awaitTerminalEvent(); + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); ts.assertNoErrors(); Set actual = new HashSet(ts.getOnNextEvents()); @@ -224,7 +224,7 @@ public void testSimpleOneLessAsyncLoop() { } @Test(timeout = 10000) public void testSimpleOneLessAsync() { - for (int i = 2; i < 100; i++) { + for (int i = 2; i < 50; i++) { TestSubscriber ts = new TestSubscriber(); List> sourceList = new ArrayList>(i); Set expected = new HashSet(i); @@ -235,7 +235,7 @@ public void testSimpleOneLessAsync() { Observable.merge(sourceList, i - 1).subscribe(ts); - ts.awaitTerminalEvent(); + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); ts.assertNoErrors(); Set actual = new HashSet(ts.getOnNextEvents()); From 047cc2863b6201fa9b1e3f3c79d545a87798f6bf Mon Sep 17 00:00:00 2001 From: David Karnok Date: Mon, 26 Jan 2015 16:41:04 +0100 Subject: [PATCH 083/857] Added value null check --- .../rx/internal/operators/OperatorOnBackpressureBuffer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index d323122f8a..e35c489d5c 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -126,7 +126,7 @@ public Object peek() { @Override public Object poll() { Object value = queue.poll(); - if (capacity != null) { + if (capacity != null && value != null) { capacity.incrementAndGet(); } return value; From 9fa580c29243924fb4cb632de2c19d1819fb12c6 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 27 Jan 2015 10:56:47 +0100 Subject: [PATCH 084/857] Changed operator names, fixed backpressure. --- src/main/java/rx/Observable.java | 4 +- .../operators/OperatorDoTakeWhile.java | 83 ---------------- .../operators/OperatorTakeUntilPredicate.java | 97 +++++++++++++++++++ ...va => OperatorTakeUntilPredicateTest.java} | 34 +++++-- 4 files changed, 125 insertions(+), 93 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OperatorDoTakeWhile.java create mode 100644 src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java rename src/test/java/rx/internal/operators/{OperatorDoTakeWhileTest.java => OperatorTakeUntilPredicateTest.java} (74%) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index ee254b3b5c..944ac11f84 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7840,8 +7840,8 @@ public final Observable takeWhile(final Func1 predicate) * @see Observable#takeWhile(Func1) */ @Experimental - public final Observable doTakeWhile(final Func1 predicate) { - return lift(new OperatorDoTakeWhile(predicate)); + public final Observable takeUntil(final Func1 predicate) { + return lift(new OperatorTakeUntilPredicate(predicate)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorDoTakeWhile.java b/src/main/java/rx/internal/operators/OperatorDoTakeWhile.java deleted file mode 100644 index 388d4ea217..0000000000 --- a/src/main/java/rx/internal/operators/OperatorDoTakeWhile.java +++ /dev/null @@ -1,83 +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 rx.Observable.Operator; -import rx.Subscriber; -import rx.annotations.Experimental; -import rx.functions.Func1; - -/** - * Returns an Observable that emits items emitted by the source Observable until - * the provided predicate returns false - *

- */ -@Experimental -public final class OperatorDoTakeWhile implements Operator { - - private final Func1 predicate; - - public OperatorDoTakeWhile(final Func1 predicate) { - this.predicate = predicate; - } - - @Override - public Subscriber call(final Subscriber child) { - Subscriber parent = new Subscriber() { - - private boolean done = false; - - @Override - public void onNext(T args) { - child.onNext(args); - - boolean doContinue = false; - try { - doContinue = predicate.call(args); - } catch (Throwable e) { - done = true; - child.onError(e); - unsubscribe(); - return; - } - if (!doContinue) { - done = true; - child.onCompleted(); - unsubscribe(); - } - } - - @Override - public void onCompleted() { - if (!done) { - child.onCompleted(); - } - } - - @Override - public void onError(Throwable e) { - if (!done) { - child.onError(e); - } - } - - }; - child.add(parent); // don't unsubscribe downstream - - return parent; - } - -} diff --git a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java new file mode 100644 index 0000000000..bb5eda0d24 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java @@ -0,0 +1,97 @@ +/** + * 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.Operator; +import rx.*; +import rx.annotations.Experimental; +import rx.functions.Func1; + +/** + * Returns an Observable that emits items emitted by the source Observable until + * the provided predicate returns false + *

+ */ +@Experimental +public final class OperatorTakeUntilPredicate implements Operator { + /** Subscriber returned to the upstream. */ + private final class ParentSubscriber extends Subscriber { + private final Subscriber child; + private boolean done = false; + + private ParentSubscriber(Subscriber child) { + this.child = child; + } + + @Override + public void onNext(T args) { + child.onNext(args); + + boolean doContinue = false; + try { + doContinue = predicate.call(args); + } catch (Throwable e) { + done = true; + child.onError(e); + unsubscribe(); + return; + } + if (!doContinue) { + done = true; + child.onCompleted(); + unsubscribe(); + } + } + + @Override + public void onCompleted() { + if (!done) { + child.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + if (!done) { + child.onError(e); + } + } + void downstreamRequest(long n) { + request(n); + } + } + + private final Func1 predicate; + + public OperatorTakeUntilPredicate(final Func1 predicate) { + this.predicate = predicate; + } + + @Override + public Subscriber call(final Subscriber child) { + final ParentSubscriber parent = new ParentSubscriber(child); + child.add(parent); // don't unsubscribe downstream + child.setProducer(new Producer() { + @Override + public void request(long n) { + parent.downstreamRequest(n); + } + }); + + return parent; + } + +} diff --git a/src/test/java/rx/internal/operators/OperatorDoTakeWhileTest.java b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java similarity index 74% rename from src/test/java/rx/internal/operators/OperatorDoTakeWhileTest.java rename to src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java index 09e717f658..f30e570693 100644 --- a/src/test/java/rx/internal/operators/OperatorDoTakeWhileTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java @@ -19,21 +19,24 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; -import org.junit.Test; +import java.util.Arrays; + +import org.junit.*; import rx.*; import rx.exceptions.TestException; import rx.functions.Func1; import rx.internal.util.UtilityFunctions; +import rx.observers.TestSubscriber; ; -public class OperatorDoTakeWhileTest { +public class OperatorTakeUntilPredicateTest { @Test public void takeEmpty() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - Observable.empty().doTakeWhile(UtilityFunctions.alwaysTrue()).subscribe(o); + Observable.empty().takeUntil(UtilityFunctions.alwaysTrue()).subscribe(o); verify(o, never()).onNext(any()); verify(o, never()).onError(any(Throwable.class)); @@ -44,7 +47,7 @@ public void takeAll() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - Observable.just(1, 2).doTakeWhile(UtilityFunctions.alwaysTrue()).subscribe(o); + Observable.just(1, 2).takeUntil(UtilityFunctions.alwaysTrue()).subscribe(o); verify(o).onNext(1); verify(o).onNext(2); @@ -56,7 +59,7 @@ public void takeFirst() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - Observable.just(1, 2).doTakeWhile(UtilityFunctions.alwaysFalse()).subscribe(o); + Observable.just(1, 2).takeUntil(UtilityFunctions.alwaysFalse()).subscribe(o); verify(o).onNext(1); verify(o, never()).onNext(2); @@ -68,7 +71,7 @@ public void takeSome() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - Observable.just(1, 2, 3).doTakeWhile(new Func1() { + Observable.just(1, 2, 3).takeUntil(new Func1() { @Override public Boolean call(Integer t1) { return t1 < 2; @@ -86,7 +89,7 @@ public void functionThrows() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - Observable.just(1, 2, 3).doTakeWhile(new Func1() { + Observable.just(1, 2, 3).takeUntil(new Func1() { @Override public Boolean call(Integer t1) { throw new TestException("Forced failure"); @@ -107,11 +110,26 @@ public void sourceThrows() { Observable.just(1) .concatWith(Observable.error(new TestException())) .concatWith(Observable.just(2)) - .doTakeWhile(UtilityFunctions.alwaysTrue()).subscribe(o); + .takeUntil(UtilityFunctions.alwaysTrue()).subscribe(o); verify(o).onNext(1); verify(o, never()).onNext(2); verify(o).onError(any(TestException.class)); verify(o, never()).onCompleted(); } + @Test + public void backpressure() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(5); + } + }; + + Observable.range(1, 1000).takeUntil(UtilityFunctions.alwaysTrue()).subscribe(ts); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); + Assert.assertEquals(0, ts.getOnCompletedEvents().size()); + } } From dad663fb1342089693e6dfc581cf1a2138b68b1a Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 28 Jan 2015 13:53:12 +1100 Subject: [PATCH 085/857] remove unnecessary imports, add missing generic types, add SuppressWarnings(unchecked) --- src/main/java/rx/exceptions/OnErrorThrowable.java | 2 +- src/main/java/rx/functions/Actions.java | 2 -- src/main/java/rx/internal/operators/BlockingOperatorNext.java | 1 + src/main/java/rx/internal/operators/OnSubscribeCache.java | 1 - src/main/java/rx/internal/operators/OperatorGroupBy.java | 3 +-- src/main/java/rx/internal/operators/OperatorObserveOn.java | 2 ++ src/main/java/rx/internal/operators/OperatorPublish.java | 2 +- src/main/java/rx/internal/operators/OperatorScan.java | 1 - src/main/java/rx/observables/BlockingObservable.java | 1 - src/main/java/rx/schedulers/CachedThreadScheduler.java | 1 + src/main/java/rx/schedulers/TrampolineScheduler.java | 3 ++- src/main/java/rx/subjects/TestSubject.java | 1 - src/main/java/rx/subscriptions/Subscriptions.java | 2 -- 13 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java index 8eae62e5ef..3a38e74401 100644 --- a/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -148,7 +148,7 @@ private static String renderValue(Object value){ return (String) value; } if (value instanceof Enum) { - return ((Enum) value).name(); + return ((Enum) value).name(); } return value.getClass().getName() + ".class"; } diff --git a/src/main/java/rx/functions/Actions.java b/src/main/java/rx/functions/Actions.java index bec18c431a..2002995487 100644 --- a/src/main/java/rx/functions/Actions.java +++ b/src/main/java/rx/functions/Actions.java @@ -15,8 +15,6 @@ */ package rx.functions; -import rx.Observer; - /** * Utility class for the Action interfaces. */ diff --git a/src/main/java/rx/internal/operators/BlockingOperatorNext.java b/src/main/java/rx/internal/operators/BlockingOperatorNext.java index 600f5b418d..05b5b8f1d8 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorNext.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorNext.java @@ -147,6 +147,7 @@ public void remove() { private static class NextObserver extends Subscriber> { private final BlockingQueue> buf = new ArrayBlockingQueue>(1); + @SuppressWarnings("unused") volatile int waiting; @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater WAITING_UPDATER diff --git a/src/main/java/rx/internal/operators/OnSubscribeCache.java b/src/main/java/rx/internal/operators/OnSubscribeCache.java index af54fc9f90..a568fd0e0b 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCache.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCache.java @@ -20,7 +20,6 @@ import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; -import rx.observers.Subscribers; import rx.subjects.ReplaySubject; import rx.subjects.Subject; diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 722a047884..ef548066f3 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -15,12 +15,10 @@ */ package rx.internal.operators; -import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -206,6 +204,7 @@ private Object groupedKey(K key) { return key == null ? NULL_KEY : key; } + @SuppressWarnings("unchecked") private K getKey(Object groupedKey) { return groupedKey == NULL_KEY ? null : (K) groupedKey; } diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 99394764a6..b11ebd660c 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -72,10 +72,12 @@ private static final class ObserveOnSubscriber extends Subscriber { private boolean completed = false; private boolean failure = false; + @SuppressWarnings("unused") private volatile long requested = 0; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "requested"); + @SuppressWarnings("unused") volatile long counter; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater COUNTER_UPDATER = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "counter"); diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 6e2328e012..ecb2145c26 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -16,7 +16,6 @@ package rx.internal.operators; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -266,6 +265,7 @@ public synchronized void removeSubscriber(Subscriber subscriber) { resetAfterSubscriberUpdate(); } + @SuppressWarnings("unchecked") private long resetAfterSubscriberUpdate() { subscribers = new Subscriber[ss.size()]; int i = 0; diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index 6809af0db3..92bb2175d2 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -23,7 +23,6 @@ import rx.exceptions.OnErrorThrowable; import rx.functions.Func0; import rx.functions.Func2; -import rx.internal.util.UtilityFunctions; /** * Returns an Observable that applies a function to the first item emitted by a source Observable, then feeds diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 477bf54bc4..8a4ce728cf 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -26,7 +26,6 @@ import rx.Subscription; import rx.functions.Action1; import rx.functions.Func1; -import rx.functions.Functions; import rx.internal.operators.BlockingOperatorLatest; import rx.internal.operators.BlockingOperatorMostRecent; import rx.internal.operators.BlockingOperatorNext; diff --git a/src/main/java/rx/schedulers/CachedThreadScheduler.java b/src/main/java/rx/schedulers/CachedThreadScheduler.java index fe70a58a43..f1cd815b64 100644 --- a/src/main/java/rx/schedulers/CachedThreadScheduler.java +++ b/src/main/java/rx/schedulers/CachedThreadScheduler.java @@ -110,6 +110,7 @@ public Worker createWorker() { private static final class EventLoopWorker extends Scheduler.Worker { private final CompositeSubscription innerSubscription = new CompositeSubscription(); private final ThreadWorker threadWorker; + @SuppressWarnings("unused") volatile int once; static final AtomicIntegerFieldUpdater ONCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(EventLoopWorker.class, "once"); diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index feb0d93ee4..1482d34756 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -47,7 +47,8 @@ public Worker createWorker() { private static class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { - private static final AtomicIntegerFieldUpdater COUNTER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(InnerCurrentThreadScheduler.class, "counter"); + private static final AtomicIntegerFieldUpdater COUNTER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(InnerCurrentThreadScheduler.class, "counter"); + @SuppressWarnings("unused") volatile int counter; private final PriorityBlockingQueue queue = new PriorityBlockingQueue(); private final BooleanSubscription innerSubscription = new BooleanSubscription(); diff --git a/src/main/java/rx/subjects/TestSubject.java b/src/main/java/rx/subjects/TestSubject.java index baebf65c7b..2400e929f1 100644 --- a/src/main/java/rx/subjects/TestSubject.java +++ b/src/main/java/rx/subjects/TestSubject.java @@ -17,7 +17,6 @@ import java.util.concurrent.TimeUnit; -import rx.Observable; import rx.Observer; import rx.Scheduler; import rx.functions.Action0; diff --git a/src/main/java/rx/subscriptions/Subscriptions.java b/src/main/java/rx/subscriptions/Subscriptions.java index 0fe84240ce..bbc075a3a9 100644 --- a/src/main/java/rx/subscriptions/Subscriptions.java +++ b/src/main/java/rx/subscriptions/Subscriptions.java @@ -16,12 +16,10 @@ package rx.subscriptions; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import rx.Subscription; import rx.annotations.Experimental; import rx.functions.Action0; -import rx.functions.Actions; /** * Helper methods and utilities for creating and working with {@link Subscription} objects From 5348ebf333b21007186b307174c517e5af879b09 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 28 Jan 2015 11:19:33 +1100 Subject: [PATCH 086/857] handle request overflow for OperatorMerge --- .../rx/internal/operators/OperatorMerge.java | 11 ++++++- .../internal/operators/OperatorMergeTest.java | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 9b0eb074aa..b86db26c47 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -545,7 +545,16 @@ public void request(long n) { if (n == Long.MAX_VALUE) { requested = Long.MAX_VALUE; } else { - REQUESTED.getAndAdd(this, n); + // add n to requested but check for overflow + while (true) { + long current = REQUESTED.get(this); + long next = current + n; + //check for overflow + if (next < 0) + next = Long.MAX_VALUE; + if (REQUESTED.compareAndSet(this, current, next)) + break; + } if (ms.drainQueuesIfNeeded()) { boolean sendComplete = false; synchronized (ms) { diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index fa861f68ea..7d785b4088 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -1183,6 +1183,38 @@ public void call() { assertTrue(a); //} } + + @Test + public void testMergeRequestOverflow() throws InterruptedException { + //do a non-trivial merge so that future optimisations with EMPTY don't invalidate this test + Observable o = Observable.from(Arrays.asList(1,2)).mergeWith(Observable.from(Arrays.asList(3,4))); + final int expectedCount = 4; + final CountDownLatch latch = new CountDownLatch(expectedCount); + o.subscribeOn(Schedulers.computation()).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + //ignore + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer t) { + latch.countDown(); + request(2); + request(Long.MAX_VALUE-1); + }}); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } private static Action1 printCount() { return new Action1() { From 8fd09fb8dfd74cb20d72692ed9659788b6a1169b Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 28 Jan 2015 16:19:37 +1100 Subject: [PATCH 087/857] Subscriber.request should throw exception if negative request made --- src/main/java/rx/Subscriber.java | 5 ++++ src/test/java/rx/SubscriberTest.java | 35 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 43f6b6d68d..def68cc24a 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -92,8 +92,13 @@ public void onStart() { * * @param n the maximum number of items you want the Observable to emit to the Subscriber at this time, or * {@code Long.MAX_VALUE} if you want the Observable to emit items at its own pace + * @throws IllegalArgumentException + * if {@code n} is negative */ protected final void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("number requested cannot be negative: " + n); + } Producer shouldRequest = null; synchronized (this) { if (p != null) { diff --git a/src/test/java/rx/SubscriberTest.java b/src/test/java/rx/SubscriberTest.java index 71daf64eb6..d52d34802d 100644 --- a/src/test/java/rx/SubscriberTest.java +++ b/src/test/java/rx/SubscriberTest.java @@ -16,9 +16,13 @@ package rx; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -419,4 +423,35 @@ public void onNext(Integer t) { assertEquals(1, c.get()); } + + @Test + public void testNegativeRequestThrowsIllegalArgumentException() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference exception = new AtomicReference(); + Observable.just(1,2,3,4).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + exception.set(e); + latch.countDown(); + } + + @Override + public void onNext(Integer t) { + request(-1); + request(1); + }}); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + assertTrue(exception.get() instanceof IllegalArgumentException); + } } From 7199792a2e9196f7194653a190547b874fa4a187 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 28 Jan 2015 23:09:22 +1100 Subject: [PATCH 088/857] onStart requests should be additive (and check for overflow) --- src/main/java/rx/Subscriber.java | 10 ++++- src/test/java/rx/SubscriberTest.java | 60 ++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index def68cc24a..237817e88a 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -103,8 +103,16 @@ protected final void request(long n) { synchronized (this) { if (p != null) { shouldRequest = p; - } else { + } else if (requested == Long.MIN_VALUE) { requested = n; + } else { + final long total = requested + n; + // check if overflow occurred + if (total < 0) { + requested = Long.MAX_VALUE; + } else { + requested = total; + } } } // after releasing lock diff --git a/src/test/java/rx/SubscriberTest.java b/src/test/java/rx/SubscriberTest.java index d52d34802d..4b0f8f3f23 100644 --- a/src/test/java/rx/SubscriberTest.java +++ b/src/test/java/rx/SubscriberTest.java @@ -18,6 +18,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -343,7 +346,6 @@ public void onError(Throwable e) { @Override public void onNext(Integer t) { - System.out.println(t); request(1); } @@ -375,7 +377,6 @@ public void onError(Throwable e) { @Override public void onNext(Integer t) { - System.out.println(t); request(1); } @@ -411,7 +412,6 @@ public void onError(Throwable e) { @Override public void onNext(Integer t) { - System.out.println(t); child.onNext(t); request(1); } @@ -454,4 +454,58 @@ public void onNext(Integer t) { assertTrue(latch.await(10, TimeUnit.SECONDS)); assertTrue(exception.get() instanceof IllegalArgumentException); } + + @Test + public void testOnStartRequestsAreAdditive() { + final List list = new ArrayList(); + Observable.just(1,2,3,4,5).subscribe(new Subscriber() { + @Override + public void onStart() { + request(3); + request(2); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + list.add(t); + }}); + assertEquals(Arrays.asList(1,2,3,4,5), list); + } + + @Test + public void testOnStartRequestsAreAdditiveAndOverflowBecomesMaxValue() { + final List list = new ArrayList(); + Observable.just(1,2,3,4,5).subscribe(new Subscriber() { + @Override + public void onStart() { + request(2); + request(Long.MAX_VALUE-1); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + list.add(t); + }}); + assertEquals(Arrays.asList(1,2,3,4,5), list); + } } From 77c5643180ab875b3fa7f4e61ce9191014b85f38 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 28 Jan 2015 15:29:59 +0100 Subject: [PATCH 089/857] Publish: fixed incorrect subscriber requested accounting --- .../internal/operators/OperatorPublish.java | 81 ++++++++++--------- .../operators/OperatorPublishTest.java | 77 +++++++++++++++--- 2 files changed, 108 insertions(+), 50 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index ecb2145c26..798517cced 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -206,7 +206,6 @@ public void onNext(T t) { */ private static class State { private long outstandingRequests = -1; - private long emittedSinceRequest = 0; private OriginSubscriber origin; // using AtomicLong to simplify mutating it, not for thread-safety since we're synchronizing access to this class // using LinkedHashMap so the order of Subscribers having onNext invoked is deterministic (same each time the code is run) @@ -225,7 +224,6 @@ public synchronized void setOrigin(OriginSubscriber o) { public synchronized boolean canEmitWithDecrement() { if (outstandingRequests > 0) { outstandingRequests--; - emittedSinceRequest++; return true; } return false; @@ -233,7 +231,6 @@ public synchronized boolean canEmitWithDecrement() { public synchronized void incrementOutstandingAfterFailedEmit() { outstandingRequests++; - emittedSinceRequest--; } public synchronized Subscriber[] getSubscribers() { @@ -243,50 +240,55 @@ public synchronized Subscriber[] getSubscribers() { /** * @return long outstandingRequests */ - public synchronized long requestFromSubscriber(Subscriber subscriber, Long request) { - AtomicLong r = ss.get(subscriber); + public synchronized long requestFromSubscriber(Subscriber subscriber, long request) { + Map, AtomicLong> subs = ss; + AtomicLong r = subs.get(subscriber); if (r == null) { - ss.put(subscriber, new AtomicLong(request)); + subs.put(subscriber, new AtomicLong(request)); } else { - if (r.get() != Long.MAX_VALUE) { - if (request == Long.MAX_VALUE) { - r.set(Long.MAX_VALUE); - } else { - r.addAndGet(request.longValue()); + do { + long current = r.get(); + if (current == Long.MAX_VALUE) { + break; } - } + long u = current + request; + if (u < 0) { + u = Long.MAX_VALUE; + } + if (r.compareAndSet(current, u)) { + break; + } + } while (true); } - return resetAfterSubscriberUpdate(); + return resetAfterSubscriberUpdate(subs); } public synchronized void removeSubscriber(Subscriber subscriber) { - ss.remove(subscriber); - resetAfterSubscriberUpdate(); + Map, AtomicLong> subs = ss; + subs.remove(subscriber); + resetAfterSubscriberUpdate(subs); } @SuppressWarnings("unchecked") - private long resetAfterSubscriberUpdate() { - subscribers = new Subscriber[ss.size()]; + private long resetAfterSubscriberUpdate(Map, AtomicLong> subs) { + Subscriber[] subscriberArray = new Subscriber[subs.size()]; int i = 0; - for (Subscriber s : ss.keySet()) { - subscribers[i++] = s; - } - long lowest = -1; - for (AtomicLong l : ss.values()) { - // decrement all we have emitted since last request - long c = l.addAndGet(-emittedSinceRequest); + for (Map.Entry, AtomicLong> e : subs.entrySet()) { + subscriberArray[i++] = e.getKey(); + AtomicLong l = e.getValue(); + long c = l.get(); if (lowest == -1 || c < lowest) { lowest = c; } } + this.subscribers = subscriberArray; /* * when receiving a request from a subscriber we reset 'outstanding' to the lowest of all subscribers */ outstandingRequests = lowest; - emittedSinceRequest = 0; - return outstandingRequests; + return lowest; } } @@ -299,7 +301,7 @@ private static class RequestHandler { @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater WIP = AtomicLongFieldUpdater.newUpdater(RequestHandler.class, "wip"); - public void requestFromChildSubscriber(Subscriber subscriber, Long request) { + public void requestFromChildSubscriber(Subscriber subscriber, long request) { state.requestFromSubscriber(subscriber, request); OriginSubscriber originSubscriber = state.getOrigin(); if(originSubscriber != null) { @@ -333,6 +335,11 @@ private void requestMoreAfterEmission(int emitted) { public void drainQueue(OriginSubscriber originSubscriber) { if (WIP.getAndIncrement(this) == 0) { + State localState = state; + Map, AtomicLong> localMap = localState.ss; + RxRingBuffer localBuffer = originSubscriber.buffer; + NotificationLite nl = notifier; + int emitted = 0; do { /* @@ -345,27 +352,23 @@ public void drainQueue(OriginSubscriber originSubscriber) { * If we want to batch this then we need to account for new subscribers arriving with a lower request count * concurrently while iterating the batch ... or accept that they won't */ + while (true) { - boolean shouldEmit = state.canEmitWithDecrement(); + boolean shouldEmit = localState.canEmitWithDecrement(); if (!shouldEmit) { break; } - Object o = originSubscriber.buffer.poll(); + Object o = localBuffer.poll(); if (o == null) { // nothing in buffer so increment outstanding back again - state.incrementOutstandingAfterFailedEmit(); + localState.incrementOutstandingAfterFailedEmit(); break; } - if (notifier.isCompleted(o)) { - for (Subscriber s : state.getSubscribers()) { - notifier.accept(s, o); - } - - } else { - for (Subscriber s : state.getSubscribers()) { - notifier.accept(s, o); - } + for (Subscriber s : localState.getSubscribers()) { + AtomicLong req = localMap.get(s); + nl.accept(s, o); + req.decrementAndGet(); } emitted++; } diff --git a/src/test/java/rx/internal/operators/OperatorPublishTest.java b/src/test/java/rx/internal/operators/OperatorPublishTest.java index 4ad552d4ab..611b1216b1 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishTest.java @@ -15,23 +15,17 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; import org.junit.Test; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observables.ConnectableObservable; import rx.observers.TestSubscriber; @@ -187,4 +181,65 @@ public Boolean call(Integer i) { System.out.println(ts.getOnNextEvents()); } + @Test(timeout = 10000) + public void testBackpressureTwoConsumers() { + final AtomicInteger sourceEmission = new AtomicInteger(); + final AtomicBoolean sourceUnsubscribed = new AtomicBoolean(); + final Observable source = Observable.range(1, 100) + .doOnNext(new Action1() { + @Override + public void call(Integer t1) { + sourceEmission.incrementAndGet(); + } + }) + .doOnUnsubscribe(new Action0() { + @Override + public void call() { + sourceUnsubscribed.set(true); + } + }).share(); + ; + + final AtomicBoolean child1Unsubscribed = new AtomicBoolean(); + final AtomicBoolean child2Unsubscribed = new AtomicBoolean(); + + final TestSubscriber ts2 = new TestSubscriber(); + + final TestSubscriber ts1 = new TestSubscriber() { + @Override + public void onNext(Integer t) { + if (getOnNextEvents().size() == 2) { + source.doOnUnsubscribe(new Action0() { + @Override + public void call() { + child2Unsubscribed.set(true); + } + }).take(5).subscribe(ts2); + } + super.onNext(t); + } + }; + + source.doOnUnsubscribe(new Action0() { + @Override + public void call() { + child1Unsubscribed.set(true); + } + }).take(5).subscribe(ts1); + + ts1.awaitTerminalEvent(); + ts2.awaitTerminalEvent(); + + ts1.assertNoErrors(); + ts2.assertNoErrors(); + + assertTrue(sourceUnsubscribed.get()); + assertTrue(child1Unsubscribed.get()); + assertTrue(child2Unsubscribed.get()); + + ts1.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); + ts2.assertReceivedOnNext(Arrays.asList(4, 5, 6, 7, 8)); + + assertEquals(8, sourceEmission.get()); + } } From 308255868f25e8f805d4e8b3f9a9d3571636b817 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 28 Jan 2015 16:48:40 +0100 Subject: [PATCH 090/857] RxRingBuffer with synchronization --- .../rx/internal/operators/OperatorMerge.java | 4 +- .../java/rx/internal/util/RxRingBuffer.java | 101 +++++++++++------- .../internal/util/unsafe/SpscArrayQueue.java | 23 ++-- 3 files changed, 77 insertions(+), 51 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index b86db26c47..384a7bea42 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -308,7 +308,7 @@ private void handleScalarSynchronousObservableWithRequestLimits(ScalarSynchronou private RxRingBuffer getOrCreateScalarValueQueue() { RxRingBuffer svq = scalarValueQueue; if (svq == null) { - svq = RxRingBuffer.getSpmcInstance(); + svq = RxRingBuffer.getSpscInstance(); scalarValueQueue = svq; } return svq; @@ -581,7 +581,7 @@ private static final class InnerSubscriber extends Subscriber { @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ONCE_TERMINATED = AtomicIntegerFieldUpdater.newUpdater(InnerSubscriber.class, "terminated"); - private final RxRingBuffer q = RxRingBuffer.getSpmcInstance(); + private final RxRingBuffer q = RxRingBuffer.getSpscInstance(); public InnerSubscriber(MergeSubscriber parent, MergeProducer producer) { this.parentSubscriber = parent; diff --git a/src/main/java/rx/internal/util/RxRingBuffer.java b/src/main/java/rx/internal/util/RxRingBuffer.java index 389793c1c3..649f8e8a6e 100644 --- a/src/main/java/rx/internal/util/RxRingBuffer.java +++ b/src/main/java/rx/internal/util/RxRingBuffer.java @@ -34,7 +34,7 @@ public class RxRingBuffer implements Subscription { public static RxRingBuffer getSpscInstance() { if (UnsafeAccess.isUnsafeAvailable()) { // TODO the SpscArrayQueue isn't ready yet so using SpmcArrayQueue for now - return new RxRingBuffer(SPMC_POOL, SIZE); + return new RxRingBuffer(SPSC_POOL, SIZE); } else { return new RxRingBuffer(); } @@ -306,12 +306,13 @@ private RxRingBuffer(ObjectPool> pool, int size) { this.size = size; } - public void release() { - if (pool != null) { - Queue q = queue; + public synchronized void release() { + Queue q = queue; + ObjectPool> p = pool; + if (p != null && q != null) { q.clear(); queue = null; - pool.returnObject(q); + p.returnObject(q); } } @@ -331,10 +332,21 @@ public void unsubscribe() { * if more onNext are sent than have been requested */ public void onNext(Object o) throws MissingBackpressureException { - if (queue == null) { + boolean iae = false; + boolean mbe = false; + synchronized (this) { + Queue q = queue; + if (q != null) { + mbe = !q.offer(on.next(o)); + } else { + iae = true; + } + } + + if (iae) { throw new IllegalStateException("This instance has been unsubscribed and the queue is no longer usable."); } - if (!queue.offer(on.next(o))) { + if (mbe) { throw new MissingBackpressureException(); } } @@ -362,55 +374,66 @@ public int capacity() { } public int count() { - if (queue == null) { + Queue q = queue; + if (q == null) { return 0; } - return queue.size(); + return q.size(); } public boolean isEmpty() { - if (queue == null) { + Queue q = queue; + if (q == null) { return true; } - return queue.isEmpty(); + return q.isEmpty(); } public Object poll() { - if (queue == null) { - // we are unsubscribed and have released the undelrying queue - return null; - } Object o; - o = queue.poll(); - /* - * benjchristensen July 10 2014 => The check for 'queue.isEmpty()' came from a very rare concurrency bug where poll() - * is invoked, then an "onNext + onCompleted/onError" arrives before hitting the if check below. In that case, - * "o == null" and there is a terminal state, but now "queue.isEmpty()" and we should NOT return the terminalState. - * - * The queue.size() check is a double-check that works to handle this, without needing to synchronize poll with on* - * or needing to enqueue terminalState. - * - * This did make me consider eliminating the 'terminalState' ref and enqueuing it ... but then that requires - * a +1 of the size, or -1 of how many onNext can be sent. See comment on 'terminalState' above for why it - * is currently the way it is. - */ - if (o == null && terminalState != null && queue.isEmpty()) { - o = terminalState; - // once emitted we clear so a poll loop will finish - terminalState = null; + synchronized (this) { + Queue q = queue; + if (q == null) { + // we are unsubscribed and have released the undelrying queue + return null; + } + o = q.poll(); + + /* + * benjchristensen July 10 2014 => The check for 'queue.isEmpty()' came from a very rare concurrency bug where poll() + * is invoked, then an "onNext + onCompleted/onError" arrives before hitting the if check below. In that case, + * "o == null" and there is a terminal state, but now "queue.isEmpty()" and we should NOT return the terminalState. + * + * The queue.size() check is a double-check that works to handle this, without needing to synchronize poll with on* + * or needing to enqueue terminalState. + * + * This did make me consider eliminating the 'terminalState' ref and enqueuing it ... but then that requires + * a +1 of the size, or -1 of how many onNext can be sent. See comment on 'terminalState' above for why it + * is currently the way it is. + */ + Object ts = terminalState; + if (o == null && ts != null && q.peek() == null) { + o = ts; + // once emitted we clear so a poll loop will finish + terminalState = null; + } } return o; } public Object peek() { - if (queue == null) { - // we are unsubscribed and have released the undelrying queue - return null; - } Object o; - o = queue.peek(); - if (o == null && terminalState != null && queue.isEmpty()) { - o = terminalState; + synchronized (this) { + Queue q = queue; + if (q == null) { + // we are unsubscribed and have released the undelrying queue + return null; + } + o = q.peek(); + Object ts = terminalState; + if (o == null && ts != null && q.peek() == null) { + o = ts; + } } return o; } diff --git a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java index 16d1e81fae..6696cbce1b 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java @@ -118,19 +118,22 @@ public SpscArrayQueue(final int capacity) { */ @Override public boolean offer(final E e) { - if (null == e) { - throw new NullPointerException("Null is not a valid element"); - } +// if (null == e) { +// throw new NullPointerException("Null is not a valid element"); +// } // local load of field to avoid repeated loads after volatile reads final E[] lElementBuffer = buffer; final long offset = calcElementOffset(producerIndex); - if (producerIndex >= producerLookAhead) { - if (null == lvElement(lElementBuffer, calcElementOffset(producerIndex + lookAheadStep))) {// LoadLoad - producerLookAhead = producerIndex + lookAheadStep; - } - else if (null != lvElement(lElementBuffer, offset)){ - return false; - } +// if (producerIndex >= producerLookAhead) { +// if (null == lvElement(lElementBuffer, calcElementOffset(producerIndex + lookAheadStep))) {// LoadLoad +// producerLookAhead = producerIndex + lookAheadStep; +// } +// else if (null != lvElement(lElementBuffer, offset)){ +// return false; +// } +// } + if (null != lvElement(lElementBuffer, offset)){ + return false; } producerIndex++; // do increment here so the ordered store give both a barrier soElement(lElementBuffer, offset, e);// StoreStore From f76861ba5d96f9daa39e83c0ac1d569a5206c65c Mon Sep 17 00:00:00 2001 From: David Gross Date: Wed, 28 Jan 2015 09:19:06 -0800 Subject: [PATCH 091/857] Correcting javadocs for the amb/ambWith operators --- src/main/java/rx/Observable.java | 70 ++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 65c8c3a3a2..f12fa649fd 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -205,7 +205,8 @@ public static interface Transformer extends Func1, Observabl */ /** - * Mirrors the one Observable in an Iterable of several Observables that first emits an item. + * Mirrors the one Observable in an Iterable of several Observables that first either emits an item or sends + * a termination notification. *

* *

@@ -215,8 +216,8 @@ public static interface Transformer extends Func1, Observabl * * @param sources * an Iterable of Observable sources competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Iterable> sources) { @@ -224,7 +225,8 @@ public final static Observable amb(Iterable * *
@@ -236,8 +238,8 @@ public final static Observable amb(IterableReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2) { @@ -245,7 +247,8 @@ public final static Observable amb(Observable o1, Observable } /** - * Given three Observables, mirrors the one that first emits an item. + * Given three Observables, mirrors the one that first either emits an item or sends a termination + * notification. *

* *

@@ -259,8 +262,8 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @param o3 * an Observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3) { @@ -268,7 +271,8 @@ public final static Observable amb(Observable o1, Observable } /** - * Given four Observables, mirrors the one that first emits an item. + * Given four Observables, mirrors the one that first either emits an item or sends a termination + * notification. *

* *

@@ -284,8 +288,8 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @param o4 * an Observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4) { @@ -293,7 +297,8 @@ public final static Observable amb(Observable o1, Observable } /** - * Given five Observables, mirrors the one that first emits an item. + * Given five Observables, mirrors the one that first either emits an item or sends a termination + * notification. *

* *

@@ -311,8 +316,8 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @param o5 * an Observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5) { @@ -320,7 +325,8 @@ public final static Observable amb(Observable o1, Observable } /** - * Given six Observables, mirrors the one that first emits an item. + * Given six Observables, mirrors the one that first either emits an item or sends a termination + * notification. *

* *

@@ -340,8 +346,8 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @param o6 * an Observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6) { @@ -349,7 +355,8 @@ public final static Observable amb(Observable o1, Observable } /** - * Given seven Observables, mirrors the one that first emits an item. + * Given seven Observables, mirrors the one that first either emits an item or sends a termination + * notification. *

* *

@@ -371,8 +378,8 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @param o7 * an Observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7) { @@ -380,7 +387,8 @@ public final static Observable amb(Observable o1, Observable } /** - * Given eight Observables, mirrors the one that first emits an item. + * Given eight Observables, mirrors the one that first either emits an item or sends a termination + * notification. *

* *

@@ -404,8 +412,8 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @param o8 * an observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8) { @@ -413,7 +421,8 @@ public final static Observable amb(Observable o1, Observable } /** - * Given nine Observables, mirrors the one that first emits an item. + * Given nine Observables, mirrors the one that first either emits an item or sends a termination + * notification. *

* *

@@ -439,8 +448,8 @@ public final static Observable amb(Observable o1, Observable * an Observable competing to react first * @param o9 * an Observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9) { @@ -2974,7 +2983,8 @@ public final Observable all(Func1 predicate) { } /** - * Mirrors the first Observable (current or provided) that emits an item. + * Mirrors the Observable (current or provided) that first either emits an item or sends a termination + * notification. *

* *

@@ -2984,8 +2994,8 @@ public final Observable all(Func1 predicate) { * * @param t1 * an Observable competing to react first - * @return an Observable that emits the same sequence of items as whichever of the source Observables first - * emitted an item + * @return an Observable that emits the same sequence as whichever of the source Observables first + * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ public final Observable ambWith(Observable t1) { From 72c56f37c58ea1cfab292d654ef363d3e8719679 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Wed, 28 Jan 2015 18:42:44 +0100 Subject: [PATCH 092/857] Update RxRingBuffer.java Fixes in comments. --- src/main/java/rx/internal/util/RxRingBuffer.java | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/main/java/rx/internal/util/RxRingBuffer.java b/src/main/java/rx/internal/util/RxRingBuffer.java index 649f8e8a6e..30d55a279a 100644 --- a/src/main/java/rx/internal/util/RxRingBuffer.java +++ b/src/main/java/rx/internal/util/RxRingBuffer.java @@ -33,7 +33,6 @@ public class RxRingBuffer implements Subscription { public static RxRingBuffer getSpscInstance() { if (UnsafeAccess.isUnsafeAvailable()) { - // TODO the SpscArrayQueue isn't ready yet so using SpmcArrayQueue for now return new RxRingBuffer(SPSC_POOL, SIZE); } else { return new RxRingBuffer(); @@ -394,23 +393,11 @@ public Object poll() { synchronized (this) { Queue q = queue; if (q == null) { - // we are unsubscribed and have released the undelrying queue + // we are unsubscribed and have released the underlying queue return null; } o = q.poll(); - /* - * benjchristensen July 10 2014 => The check for 'queue.isEmpty()' came from a very rare concurrency bug where poll() - * is invoked, then an "onNext + onCompleted/onError" arrives before hitting the if check below. In that case, - * "o == null" and there is a terminal state, but now "queue.isEmpty()" and we should NOT return the terminalState. - * - * The queue.size() check is a double-check that works to handle this, without needing to synchronize poll with on* - * or needing to enqueue terminalState. - * - * This did make me consider eliminating the 'terminalState' ref and enqueuing it ... but then that requires - * a +1 of the size, or -1 of how many onNext can be sent. See comment on 'terminalState' above for why it - * is currently the way it is. - */ Object ts = terminalState; if (o == null && ts != null && q.peek() == null) { o = ts; From dc5bf0d9d8864ba49a14dbdf33758bad9b422025 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Wed, 28 Jan 2015 18:43:57 +0100 Subject: [PATCH 093/857] Update RxRingBuffer.java Yet another undelrying --- src/main/java/rx/internal/util/RxRingBuffer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/util/RxRingBuffer.java b/src/main/java/rx/internal/util/RxRingBuffer.java index 30d55a279a..7498b445be 100644 --- a/src/main/java/rx/internal/util/RxRingBuffer.java +++ b/src/main/java/rx/internal/util/RxRingBuffer.java @@ -413,7 +413,7 @@ public Object peek() { synchronized (this) { Queue q = queue; if (q == null) { - // we are unsubscribed and have released the undelrying queue + // we are unsubscribed and have released the underlying queue return null; } o = q.peek(); From 84d38bf0485ca07f01f2871882baba8d291bbd9c Mon Sep 17 00:00:00 2001 From: David Karnok Date: Wed, 28 Jan 2015 18:59:24 +0100 Subject: [PATCH 094/857] Removed commented-out code. --- .../java/rx/internal/util/unsafe/SpscArrayQueue.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java index 6696cbce1b..3410a625f6 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java @@ -118,20 +118,9 @@ public SpscArrayQueue(final int capacity) { */ @Override public boolean offer(final E e) { -// if (null == e) { -// throw new NullPointerException("Null is not a valid element"); -// } // local load of field to avoid repeated loads after volatile reads final E[] lElementBuffer = buffer; final long offset = calcElementOffset(producerIndex); -// if (producerIndex >= producerLookAhead) { -// if (null == lvElement(lElementBuffer, calcElementOffset(producerIndex + lookAheadStep))) {// LoadLoad -// producerLookAhead = producerIndex + lookAheadStep; -// } -// else if (null != lvElement(lElementBuffer, offset)){ -// return false; -// } -// } if (null != lvElement(lElementBuffer, offset)){ return false; } From f78903f4e5ddbe5a18820efec03b4382568a8565 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 29 Jan 2015 10:42:47 +0100 Subject: [PATCH 095/857] Updating queue code from JCTools --- .../unsafe/ConcurrentCircularArrayQueue.java | 24 +++-- ...ConcurrentSequencedCircularArrayQueue.java | 7 +- .../util/unsafe/MessagePassingQueue.java | 14 +-- .../internal/util/unsafe/MpmcArrayQueue.java | 89 +++++++++++-------- .../internal/util/unsafe/SpmcArrayQueue.java | 45 ++++++---- .../internal/util/unsafe/SpscArrayQueue.java | 68 ++++++++------ .../rx/internal/util/JCToolsQueueTests.java | 52 +++++++++++ 7 files changed, 202 insertions(+), 97 deletions(-) create mode 100644 src/test/java/rx/internal/util/JCToolsQueueTests.java diff --git a/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java b/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java index 59a5800859..86a8db0b19 100644 --- a/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/ConcurrentCircularArrayQueue.java @@ -60,17 +60,16 @@ public abstract class ConcurrentCircularArrayQueue extends ConcurrentCircular REF_ARRAY_BASE = UnsafeAccess.UNSAFE.arrayBaseOffset(Object[].class) + (BUFFER_PAD << (REF_ELEMENT_SHIFT - SPARSE_SHIFT)); } - protected final int capacity; protected final long mask; // @Stable :( protected final E[] buffer; @SuppressWarnings("unchecked") public ConcurrentCircularArrayQueue(int capacity) { - this.capacity = Pow2.roundToPowerOfTwo(capacity); - mask = this.capacity - 1; + int actualCapacity = Pow2.roundToPowerOfTwo(capacity); + mask = actualCapacity - 1; // pad data on either end with some empty slots. - buffer = (E[]) new Object[(this.capacity << SPARSE_SHIFT) + BUFFER_PAD * 2]; + buffer = (E[]) new Object[(actualCapacity << SPARSE_SHIFT) + BUFFER_PAD * 2]; } /** @@ -78,9 +77,16 @@ public ConcurrentCircularArrayQueue(int capacity) { * @return the offset in bytes within the array for a given index. */ protected final long calcElementOffset(long index) { + return calcElementOffset(index, mask); + } + /** + * @param index desirable element index + * @param mask + * @return the offset in bytes within the array for a given index. + */ + protected final long calcElementOffset(long index, long mask) { return REF_ARRAY_BASE + ((index & mask) << REF_ELEMENT_SHIFT); } - /** * A plain store (no ordering/fences) of an element to a given offset * @@ -171,4 +177,10 @@ protected final E lvElement(E[] buffer, long offset) { public Iterator iterator() { throw new UnsupportedOperationException(); } -} \ No newline at end of file + @Override + public void clear() { + // we have to test isEmpty because of the weaker poll() guarantee + while (poll() != null || !isEmpty()) + ; + } +} diff --git a/src/main/java/rx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue.java b/src/main/java/rx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue.java index eb559297ad..eaf824e95e 100644 --- a/src/main/java/rx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/ConcurrentSequencedCircularArrayQueue.java @@ -35,9 +35,10 @@ public abstract class ConcurrentSequencedCircularArrayQueue extends Concurren public ConcurrentSequencedCircularArrayQueue(int capacity) { super(capacity); + int actualCapacity = (int) (this.mask + 1); // pad data on either end with some empty slots. - sequenceBuffer = new long[(this.capacity << SPARSE_SHIFT) + BUFFER_PAD * 2]; - for (long i = 0; i < this.capacity; i++) { + sequenceBuffer = new long[(actualCapacity << SPARSE_SHIFT) + BUFFER_PAD * 2]; + for (long i = 0; i < actualCapacity; i++) { soSequence(sequenceBuffer, calcSequenceOffset(i), i); } } @@ -54,4 +55,4 @@ protected final long lvSequence(long[] buffer, long offset) { return UNSAFE.getLongVolatile(buffer, offset); } -} \ No newline at end of file +} diff --git a/src/main/java/rx/internal/util/unsafe/MessagePassingQueue.java b/src/main/java/rx/internal/util/unsafe/MessagePassingQueue.java index b9f9b60ee3..a6fad8b455 100644 --- a/src/main/java/rx/internal/util/unsafe/MessagePassingQueue.java +++ b/src/main/java/rx/internal/util/unsafe/MessagePassingQueue.java @@ -33,26 +33,26 @@ interface MessagePassingQueue { /** * Called from a producer thread subject to the restrictions appropriate to the implementation and according to the - * {@link Queue#offer(Object)} interface (but failure to offer doesn't necessitate queue is full). + * {@link Queue#offer(Object)} interface. * * @param message - * @return true if element was inserted into the queue, false if cannot enqueue + * @return true if element was inserted into the queue, false iff full */ boolean offer(M message); /** * Called from the consumer thread subject to the restrictions appropriate to the implementation and according to - * the {@link Queue#poll()} interface (barring the hard requirement on null returns). + * the {@link Queue#poll()} interface. * - * @return a message from the queue if one is available, null otherwise(not necessarily empty) + * @return a message from the queue if one is available, null iff empty */ M poll(); /** * Called from the consumer thread subject to the restrictions appropriate to the implementation and according to - * the {@link Queue#peek()} interface (barring the hard requirement on null returns). + * the {@link Queue#peek()} interface. * - * @return a message from the queue if one is available, null otherwise(not necessarily empty) + * @return a message from the queue if one is available, null iff empty */ M peek(); @@ -71,4 +71,4 @@ interface MessagePassingQueue { */ boolean isEmpty(); -} \ No newline at end of file +} diff --git a/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java b/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java index 1ce7796104..8333723036 100644 --- a/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java @@ -31,8 +31,8 @@ abstract class MpmcArrayQueueProducerField extends MpmcArrayQueueL1Pad { private final static long P_INDEX_OFFSET; static { try { - P_INDEX_OFFSET = - UNSAFE.objectFieldOffset(MpmcArrayQueueProducerField.class.getDeclaredField("producerIndex")); + P_INDEX_OFFSET = UNSAFE.objectFieldOffset(MpmcArrayQueueProducerField.class + .getDeclaredField("producerIndex")); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -65,8 +65,8 @@ abstract class MpmcArrayQueueConsumerField extends MpmcArrayQueueL2Pad { private final static long C_INDEX_OFFSET; static { try { - C_INDEX_OFFSET = - UNSAFE.objectFieldOffset(MpmcArrayQueueConsumerField.class.getDeclaredField("consumerIndex")); + C_INDEX_OFFSET = UNSAFE.objectFieldOffset(MpmcArrayQueueConsumerField.class + .getDeclaredField("consumerIndex")); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -87,26 +87,28 @@ protected final boolean casConsumerIndex(long expect, long newValue) { } /** - * A Multi-Producer-Multi-Consumer queue based on a {@link ConcurrentCircularArrayQueue}. This implies that any and all - * threads may call the offer/poll/peek methods and correctness is maintained.
+ * A Multi-Producer-Multi-Consumer queue based on a {@link ConcurrentCircularArrayQueue}. This implies that + * any and all threads may call the offer/poll/peek methods and correctness is maintained.
* This implementation follows patterns documented on the package level for False Sharing protection.
* The algorithm for offer/poll is an adaptation of the one put forward by D. Vyukov (See here). The original algorithm - * uses an array of structs which should offer nice locality properties but is sadly not possible in Java (waiting on - * Value Types or similar). The alternative explored here utilizes 2 arrays, one for each field of the struct. There is - * a further alternative in the experimental project which uses iteration phase markers to achieve the same algo and is - * closer structurally to the original, but sadly does not perform as well as this implementation.
+ * href="http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue">here). The original + * algorithm uses an array of structs which should offer nice locality properties but is sadly not possible in + * Java (waiting on Value Types or similar). The alternative explored here utilizes 2 arrays, one for each + * field of the struct. There is a further alternative in the experimental project which uses iteration phase + * markers to achieve the same algo and is closer structurally to the original, but sadly does not perform as + * well as this implementation.
* Tradeoffs to keep in mind: *
    - *
  1. Padding for false sharing: counter fields and queue fields are all padded as well as either side of both arrays. - * We are trading memory to avoid false sharing(active and passive). - *
  2. 2 arrays instead of one: The algorithm requires an extra array of longs matching the size of the elements array. - * This is doubling/tripling the memory allocated for the buffer. + *
  3. Padding for false sharing: counter fields and queue fields are all padded as well as either side of + * both arrays. We are trading memory to avoid false sharing(active and passive). + *
  4. 2 arrays instead of one: The algorithm requires an extra array of longs matching the size of the + * elements array. This is doubling/tripling the memory allocated for the buffer. *
  5. Power of 2 capacity: Actual elements buffer (and sequence buffer) is the closest power of 2 larger or * equal to the requested capacity. *
* - * @param type of the element stored in the {@link java.util.Queue} + * @param + * type of the element stored in the {@link java.util.Queue} */ public class MpmcArrayQueue extends MpmcArrayQueueConsumerField { long p40, p41, p42, p43, p44, p45, p46; @@ -123,10 +125,11 @@ public boolean offer(final E e) { } // local load of field to avoid repeated loads after volatile reads + final long capacity = mask + 1; final long[] lSequenceBuffer = sequenceBuffer; long currentProducerIndex; long seqOffset; - + long cIndex = Long.MAX_VALUE;// start with bogus value, hope we don't need it while (true) { currentProducerIndex = lvProducerIndex(); // LoadLoad seqOffset = calcSequenceOffset(currentProducerIndex); @@ -140,8 +143,10 @@ public boolean offer(final E e) { break; } // failed cas, retry 1 - } else if (delta < 0) { - // poll has not moved this value forward + } else if (delta < 0 && // poll has not moved this value forward + currentProducerIndex - capacity <= cIndex && // test against cached cIndex + currentProducerIndex - capacity <= (cIndex = lvConsumerIndex())) { // test against latest cIndex + // Extra check required to ensure [Queue.offer == false iff queue is full] return false; } @@ -161,8 +166,9 @@ public boolean offer(final E e) { /** * {@inheritDoc} - * Because return null indicates queue is empty we cannot simply rely on next element visibility for poll and must - * test producer index when next element is not visible. + *

+ * Because return null indicates queue is empty we cannot simply rely on next element visibility for poll + * and must test producer index when next element is not visible. */ @Override public E poll() { @@ -170,7 +176,7 @@ public E poll() { final long[] lSequenceBuffer = sequenceBuffer; long currentConsumerIndex; long seqOffset; - + long pIndex = -1; // start with bogus value, hope we don't need it while (true) { currentConsumerIndex = lvConsumerIndex();// LoadLoad seqOffset = calcSequenceOffset(currentConsumerIndex); @@ -183,12 +189,10 @@ public E poll() { break; } // failed cas, retry 1 - } else if (delta < 0) { - // COMMENTED OUT: strict empty check. -// if (currentConsumerIndex == lvProducerIndex()) { -// return null; -// } - // next element is not visible, probably empty + } else if (delta < 0 && // slot has not been moved by producer + currentConsumerIndex >= pIndex && // test against cached pIndex + currentConsumerIndex == (pIndex = lvProducerIndex())) { // update pIndex if we must + // strict empty check, this ensures [Queue.poll() == null iff isEmpty()] return null; } @@ -202,22 +206,31 @@ public E poll() { // Move sequence ahead by capacity, preparing it for next offer // (seeing this value from a consumer will lead to retry 2) - soSequence(lSequenceBuffer, seqOffset, currentConsumerIndex + capacity);// StoreStore + soSequence(lSequenceBuffer, seqOffset, currentConsumerIndex + mask + 1);// StoreStore return e; } @Override public E peek() { - return lpElement(calcElementOffset(lvConsumerIndex())); + long currConsumerIndex; + E e; + do { + currConsumerIndex = lvConsumerIndex(); + // other consumers may have grabbed the element, or queue might be empty + e = lpElement(calcElementOffset(currConsumerIndex)); + // only return null if queue is empty + } while (e == null && currConsumerIndex != lvProducerIndex()); + return e; } @Override public int size() { /* - * It is possible for a thread to be interrupted or reschedule between the read of the producer and consumer - * indices, therefore protection is required to ensure size is within valid range. In the event of concurrent - * polls/offers to this method the size is OVER estimated as we read consumer index BEFORE the producer index. + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. */ long after = lvConsumerIndex(); while (true) { @@ -229,13 +242,13 @@ public int size() { } } } - + @Override public boolean isEmpty() { - // Order matters! + // Order matters! // Loading consumer before producer allows for producer increments after consumer index is read. - // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there is nothing we - // can do to make this an exact method. + // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there is + // nothing we can do to make this an exact method. return (lvConsumerIndex() == lvProducerIndex()); } -} \ No newline at end of file +} diff --git a/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java index 932d0e6460..ebf79f0708 100644 --- a/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java @@ -16,6 +16,8 @@ */ package rx.internal.util.unsafe; +import static rx.internal.util.unsafe.UnsafeAccess.UNSAFE; + abstract class SpmcArrayQueueL1Pad extends ConcurrentCircularArrayQueue { long p10, p11, p12, p13, p14, p15, p16; long p30, p31, p32, p33, p34, p35, p36, p37; @@ -30,7 +32,7 @@ abstract class SpmcArrayQueueProducerField extends SpmcArrayQueueL1Pad { static { try { P_INDEX_OFFSET = - UnsafeAccess.UNSAFE.objectFieldOffset(SpmcArrayQueueProducerField.class.getDeclaredField("producerIndex")); + UNSAFE.objectFieldOffset(SpmcArrayQueueProducerField.class.getDeclaredField("producerIndex")); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -42,7 +44,7 @@ protected final long lvProducerIndex() { } protected final void soTail(long v) { - UnsafeAccess.UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, v); + UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, v); } public SpmcArrayQueueProducerField(int capacity) { @@ -64,7 +66,7 @@ abstract class SpmcArrayQueueConsumerField extends SpmcArrayQueueL2Pad { static { try { C_INDEX_OFFSET = - UnsafeAccess.UNSAFE.objectFieldOffset(SpmcArrayQueueConsumerField.class.getDeclaredField("consumerIndex")); + UNSAFE.objectFieldOffset(SpmcArrayQueueConsumerField.class.getDeclaredField("consumerIndex")); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -80,7 +82,7 @@ protected final long lvConsumerIndex() { } protected final boolean casHead(long expect, long newValue) { - return UnsafeAccess.UNSAFE.compareAndSwapLong(this, C_INDEX_OFFSET, expect, newValue); + return UNSAFE.compareAndSwapLong(this, C_INDEX_OFFSET, expect, newValue); } } @@ -132,17 +134,18 @@ public boolean offer(final E e) { throw new NullPointerException("Null is not a valid element"); } final E[] lb = buffer; + final long lMask = mask; final long currProducerIndex = lvProducerIndex(); final long offset = calcElementOffset(currProducerIndex); if (null != lvElement(lb, offset)) { - // strict check as per https://github.com/JCTools/JCTools/issues/21#issuecomment-50204120 - int size = (int) (currProducerIndex - lvConsumerIndex()); - if (size == capacity) { + long size = currProducerIndex - lvConsumerIndex(); + + if(size > lMask) { return false; } else { // spin wait for slot to clear, buggers wait freedom - while (null != lvElement(lb, offset)); + while(null != lvElement(lb, offset)); } } spElement(lb, offset, e); @@ -152,14 +155,6 @@ public boolean offer(final E e) { return true; } - /** - * {@inheritDoc} - *

- * Note that we are not doing the the whole poll/tryPoll thing here like we do in MPMC/MPSC, that is because the - * problem we try to solve there is caused by having multiple producers making progress concurrently which can - * create 'bubbles' of claimed but not fully visible elements in the queue. For a single producer the problem - * doesn't exist. - */ @Override public E poll() { long currentConsumerIndex; @@ -188,7 +183,21 @@ public E poll() { @Override public E peek() { - return lvElement(calcElementOffset(lvConsumerIndex())); + long currentConsumerIndex; + final long currProducerIndexCache = lvProducerIndexCache(); + E e; + do { + currentConsumerIndex = lvConsumerIndex(); + if (currentConsumerIndex >= currProducerIndexCache) { + long currProducerIndex = lvProducerIndex(); + if (currentConsumerIndex >= currProducerIndex) { + return null; + } else { + svProducerIndexCache(currProducerIndex); + } + } + } while (null == (e = lvElement(calcElementOffset(currentConsumerIndex)))); + return e; } @Override @@ -217,4 +226,4 @@ public boolean isEmpty() { // something we can fix here. return (lvConsumerIndex() == lvProducerIndex()); } -} \ No newline at end of file +} diff --git a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java index 3410a625f6..6064106503 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java @@ -16,8 +16,10 @@ */ package rx.internal.util.unsafe; +import static rx.internal.util.unsafe.UnsafeAccess.UNSAFE; + abstract class SpscArrayQueueColdField extends ConcurrentCircularArrayQueue { - private static final Integer MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctoolts.spsc.max.lookahead.step", 4096); + private static final Integer MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); protected final int lookAheadStep; public SpscArrayQueueColdField(int capacity) { super(capacity); @@ -34,11 +36,11 @@ public SpscArrayQueueL1Pad(int capacity) { } abstract class SpscArrayQueueProducerFields extends SpscArrayQueueL1Pad { - private final static long P_INDEX_OFFSET; + protected final static long P_INDEX_OFFSET; static { try { P_INDEX_OFFSET = - UnsafeAccess.UNSAFE.objectFieldOffset(SpscArrayQueueProducerFields.class.getDeclaredField("producerIndex")); + UNSAFE.objectFieldOffset(SpscArrayQueueProducerFields.class.getDeclaredField("producerIndex")); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -49,9 +51,6 @@ abstract class SpscArrayQueueProducerFields extends SpscArrayQueueL1Pad { public SpscArrayQueueProducerFields(int capacity) { super(capacity); } - protected final long lvProducerIndex() { - return UnsafeAccess.UNSAFE.getLongVolatile(this, P_INDEX_OFFSET); - } } abstract class SpscArrayQueueL2Pad extends SpscArrayQueueProducerFields { @@ -65,11 +64,11 @@ public SpscArrayQueueL2Pad(int capacity) { abstract class SpscArrayQueueConsumerField extends SpscArrayQueueL2Pad { protected long consumerIndex; - private final static long C_INDEX_OFFSET; + protected final static long C_INDEX_OFFSET; static { try { C_INDEX_OFFSET = - UnsafeAccess.UNSAFE.objectFieldOffset(SpscArrayQueueConsumerField.class.getDeclaredField("consumerIndex")); + UNSAFE.objectFieldOffset(SpscArrayQueueConsumerField.class.getDeclaredField("consumerIndex")); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } @@ -77,9 +76,6 @@ abstract class SpscArrayQueueConsumerField extends SpscArrayQueueL2Pad { public SpscArrayQueueConsumerField(int capacity) { super(capacity); } - protected final long lvConsumerIndex() { - return UnsafeAccess.UNSAFE.getLongVolatile(this, C_INDEX_OFFSET); - } } abstract class SpscArrayQueueL3Pad extends SpscArrayQueueConsumerField { @@ -92,14 +88,16 @@ public SpscArrayQueueL3Pad(int capacity) { } /** - * A Single-Producer-Single-Consumer queue backed by a pre-allocated buffer.
This implementation is a mashup of the - * Fast Flow algorithm with an optimization of the offer - * method taken from the BQueue algorithm (a - * variation on Fast Flow).
- * For convenience the relevant papers are available in the resources folder:
- * 2010 - Pisa - SPSC Queues on Shared Cache Multi-Core Systems.pdf
- * 2012 - Junchang- BQueue- Efficient and Practical Queuing.pdf
- * This implementation is wait free. + * A Single-Producer-Single-Consumer queue backed by a pre-allocated buffer. + *

+ * This implementation is a mashup of the Fast Flow + * algorithm with an optimization of the offer method taken from the BQueue algorithm (a variation on Fast + * Flow), and adjusted to comply with Queue.offer semantics with regards to capacity.
+ * For convenience the relevant papers are available in the resources folder:
+ * 2010 - Pisa - SPSC Queues on Shared Cache Multi-Core Systems.pdf
+ * 2012 - Junchang- BQueue- Efficient and Practical Queuing.pdf
+ *
This implementation is wait free. * * @author nitsanw * @@ -120,14 +118,16 @@ public SpscArrayQueue(final int capacity) { public boolean offer(final E e) { // local load of field to avoid repeated loads after volatile reads final E[] lElementBuffer = buffer; - final long offset = calcElementOffset(producerIndex); + final long index = producerIndex; + final long offset = calcElementOffset(index); if (null != lvElement(lElementBuffer, offset)){ return false; } - producerIndex++; // do increment here so the ordered store give both a barrier - soElement(lElementBuffer, offset, e);// StoreStore + soProducerIndex(index + 1); // ordered store -> atomic and ordered for size() + soElement(lElementBuffer, offset, e); // StoreStore return true; - } + } + /** * {@inheritDoc} *

@@ -135,14 +135,15 @@ public boolean offer(final E e) { */ @Override public E poll() { - final long offset = calcElementOffset(consumerIndex); + final long index = consumerIndex; + final long offset = calcElementOffset(index); // local load of field to avoid repeated loads after volatile reads final E[] lElementBuffer = buffer; final E e = lvElement(lElementBuffer, offset);// LoadLoad if (null == e) { return null; } - consumerIndex++; // do increment here so the ordered store give both a barrier + soConsumerIndex(index + 1); // ordered store -> atomic and ordered for size() soElement(lElementBuffer, offset, null);// StoreStore return e; } @@ -174,4 +175,21 @@ public int size() { } } } + + private void soProducerIndex(long v) { + UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, v); + } + + private void soConsumerIndex(long v) { + UNSAFE.putOrderedLong(this, C_INDEX_OFFSET, v); + } + + private long lvProducerIndex() { + return UNSAFE.getLongVolatile(this, P_INDEX_OFFSET); + } + + private long lvConsumerIndex() { + return UNSAFE.getLongVolatile(this, C_INDEX_OFFSET); + } } + diff --git a/src/test/java/rx/internal/util/JCToolsQueueTests.java b/src/test/java/rx/internal/util/JCToolsQueueTests.java new file mode 100644 index 0000000000..2645dcd1c1 --- /dev/null +++ b/src/test/java/rx/internal/util/JCToolsQueueTests.java @@ -0,0 +1,52 @@ + /** + * 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.util; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import rx.internal.util.unsafe.*; + +public class JCToolsQueueTests { + @Test + public void testMpmcOfferUpToCapacity() { + int n = 128; + MpmcArrayQueue queue = new MpmcArrayQueue(n); + for (int i = 0; i < n; i++) { + assertTrue(queue.offer(i)); + } + assertFalse(queue.offer(n)); + } + @Test + public void testSpscOfferUpToCapacity() { + int n = 128; + SpscArrayQueue queue = new SpscArrayQueue(n); + for (int i = 0; i < n; i++) { + assertTrue(queue.offer(i)); + } + assertFalse(queue.offer(n)); + } + @Test + public void testSpmcOfferUpToCapacity() { + int n = 128; + SpmcArrayQueue queue = new SpmcArrayQueue(n); + for (int i = 0; i < n; i++) { + assertTrue(queue.offer(i)); + } + assertFalse(queue.offer(n)); + } +} From aeb879a8afa7d78e96c32bdd76a519f9ff4279bd Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 29 Jan 2015 10:57:01 +0100 Subject: [PATCH 096/857] Handle concurrent unsubscription in drain (avoid NPE). --- src/main/java/rx/internal/operators/OperatorPublish.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 798517cced..41041f9846 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -367,8 +367,10 @@ public void drainQueue(OriginSubscriber originSubscriber) { for (Subscriber s : localState.getSubscribers()) { AtomicLong req = localMap.get(s); - nl.accept(s, o); - req.decrementAndGet(); + if (req != null) { // null req indicates a concurrent unsubscription happened + nl.accept(s, o); + req.decrementAndGet(); + } } emitted++; } From 50e74ebb6c1db0b7ac79a06f403a193f39066989 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 29 Jan 2015 11:44:09 +0100 Subject: [PATCH 097/857] Split error conditions into separate test lines. --- .../internal/operators/OperatorRetryTest.java | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index 7783eadb2d..32a3e8f8ed 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -15,33 +15,22 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.Mockito; +import org.mockito.*; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; +import rx.Observable; import rx.Observer; -import rx.Producer; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observables.GroupedObservable; import rx.observers.TestSubscriber; @@ -722,7 +711,10 @@ public void testRetryWithBackpressureParallel() throws InterruptedException { int ncpu = Runtime.getRuntime().availableProcessors(); ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 1)); final AtomicInteger timeouts = new AtomicInteger(); - final AtomicInteger data = new AtomicInteger(); + final Map> data = new ConcurrentHashMap>(); + final Map> exceptions = new ConcurrentHashMap>(); + final Map completions = new ConcurrentHashMap(); + int m = 2000; final CountDownLatch cdl = new CountDownLatch(m); for (int i = 0; i < m; i++) { @@ -737,13 +729,13 @@ public void run() { origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); ts.awaitTerminalEvent(2, TimeUnit.SECONDS); if (ts.getOnNextEvents().size() != NUM_RETRIES + 2) { - data.incrementAndGet(); + data.put(j, ts.getOnNextEvents()); } if (ts.getOnErrorEvents().size() != 0) { - data.incrementAndGet(); + exceptions.put(j, ts.getOnErrorEvents()); } if (ts.getOnCompletedEvents().size() != 1) { - data.incrementAndGet(); + completions.put(j, ts.getOnCompletedEvents().size()); } } catch (Throwable t) { timeouts.incrementAndGet(); @@ -756,8 +748,15 @@ public void run() { exec.shutdown(); cdl.await(); assertEquals(0, timeouts.get()); - assertEquals(0, data.get()); - + if (data.size() > 0) { + fail("Data content mismatch: " + data); + } + if (exceptions.size() > 0) { + fail("Exceptions received: " + exceptions); + } + if (completions.size() > 0) { + fail("Multiple completions received: " + completions); + } } @Test(timeout = 3000) public void testIssue1900() throws InterruptedException { From 5620bf71906ce1b3e3320e8599a28b4d87df1fa2 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 29 Jan 2015 12:17:12 +0100 Subject: [PATCH 098/857] Obstruction detection in tests. --- src/test/java/rx/BackpressureTests.java | 22 ++--- .../rx/test/TestObstructionDetection.java | 90 +++++++++++++++++++ .../rx/test/TestObstructionDetectionTest.java | 74 +++++++++++++++ 3 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/test/java/rx/test/TestObstructionDetection.java create mode 100644 src/test/java/rx/test/TestObstructionDetectionTest.java diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index 0760350f67..f54b8b67d5 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -15,28 +15,29 @@ */ package rx; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; -import org.junit.Test; +import org.junit.*; import rx.Observable.OnSubscribe; import rx.exceptions.MissingBackpressureException; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.test.TestObstructionDetection; public class BackpressureTests { + @After + public void doAfterTest() { + TestObstructionDetection.checkObstruction(); + } + @Test public void testObserveOn() { int NUM = (int) (RxRingBuffer.SIZE * 2.1); @@ -163,6 +164,7 @@ public Observable call(Integer i) { } @Test + @Ignore // the test is non-deterministic and can't be made deterministic public void testFlatMapAsync() { int NUM = (int) (RxRingBuffer.SIZE * 2.1); AtomicInteger c = new AtomicInteger(); diff --git a/src/test/java/rx/test/TestObstructionDetection.java b/src/test/java/rx/test/TestObstructionDetection.java new file mode 100644 index 0000000000..859d138dd0 --- /dev/null +++ b/src/test/java/rx/test/TestObstructionDetection.java @@ -0,0 +1,90 @@ +/** + * 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.test; + +import java.util.*; +import java.util.concurrent.*; + +import rx.Scheduler; +import rx.functions.Action0; +import rx.schedulers.Schedulers; + +/** + * Check if there is an obstruction in the computation scheduler. + * Put the following code into test classes: + *

+ * @org.junit.After
+ * public void doAfterTest() {
+ *     rx.test.TestObstructionDetection.checkObstruction();
+ * }
+ * 
+ * or + *

+ * @org.junit.AfterClass
+ * public static void doAfterClass() {
+ *     rx.test.TestObstructionDetection.checkObstruction();
+ * }
+ * 
+ */ +public final class TestObstructionDetection { + private TestObstructionDetection() { + throw new IllegalStateException("No instances!"); + } + /** + * Checks if tasks can be immediately executed on the computation scheduler. + * @throws ObstructionExceptio if the schedulers don't respond within 1 second + */ + public static void checkObstruction() { + final int ncpu = Runtime.getRuntime().availableProcessors(); + + final CountDownLatch cdl = new CountDownLatch(ncpu); + final List workers = new ArrayList(); + final Action0 task = new Action0() { + @Override + public void call() { + cdl.countDown(); + } + }; + + for (int i = 0; i < ncpu; i++) { + workers.add(Schedulers.computation().createWorker()); + } + for (Scheduler.Worker w : workers) { + w.schedule(task); + } + try { + if (!cdl.await(1, TimeUnit.SECONDS)) { + throw new ObstructionException("Obstruction/Timeout detected!"); + } + } catch (InterruptedException ex) { + throw new ObstructionException("Interrupted: " + ex); + } finally { + for (Scheduler.Worker w : workers) { + w.unsubscribe(); + } + } + } + /** + * Exception thrown if obstruction was detected. + */ + public static final class ObstructionException extends RuntimeException { + /** */ + private static final long serialVersionUID = -6380717994471291795L; + public ObstructionException(String message) { + super(message); + } + } +} diff --git a/src/test/java/rx/test/TestObstructionDetectionTest.java b/src/test/java/rx/test/TestObstructionDetectionTest.java new file mode 100644 index 0000000000..f6db1db597 --- /dev/null +++ b/src/test/java/rx/test/TestObstructionDetectionTest.java @@ -0,0 +1,74 @@ +/** + * 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.test; + +import org.junit.*; + +import rx.*; +import rx.Scheduler.Worker; +import rx.functions.Action0; +import rx.schedulers.Schedulers; +import rx.test.TestObstructionDetection.ObstructionException; + +public class TestObstructionDetectionTest { + private static Scheduler.Worker w; + @org.junit.After + public void doAfterTest() { + rx.test.TestObstructionDetection.checkObstruction(); + } + @AfterClass + public static void afterClass() { + Worker w2 = w; + if (w2 != null) { + w2.unsubscribe(); + } + } + @Test(timeout = 10000, expected = ObstructionException.class) + public void testObstruction() { + Scheduler.Worker w = Schedulers.computation().createWorker(); + + try { + w.schedule(new Action0() { + @Override + public void call() { + try { + Thread.sleep(5000); + } catch (InterruptedException ex) { + + } + } + }); + TestObstructionDetection.checkObstruction(); + } finally { + w.unsubscribe(); + } + } + @Test(timeout = 10000) + public void testNoObstruction() { + w = Schedulers.computation().createWorker(); + + w.schedule(new Action0() { + @Override + public void call() { + try { + Thread.sleep(500); + } catch (InterruptedException ex) { + + } + } + }); + } +} From cc522a581398d76dd835a95a0d3e8afec003dceb Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 29 Jan 2015 12:59:41 +0100 Subject: [PATCH 099/857] CombineLatest: fixed concurrent requestUpTo yielding -1 requests sometimes. --- .../internal/operators/OnSubscribeCombineLatest.java | 11 ++++++++--- .../operators/OnSubscribeCombineLatestTest.java | 7 +++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java index 658f941430..c5ea0d1291 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java @@ -230,9 +230,14 @@ public MultiSourceRequestableSubscriber(int index, int initial, Subscriber combineLatestFunction = getConcatStringIntegerCombineLatestFunction(); From 02442238766e644ee05fbbd1340d58ae25f91275 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 29 Jan 2015 15:22:50 +0100 Subject: [PATCH 100/857] RefCount: disconnect all if upstream terminates --- .../operators/OnSubscribeRefCount.java | 59 +++++++--- .../operators/OnSubscribeRefCountTest.java | 107 +++++++++++++----- 2 files changed, 124 insertions(+), 42 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRefCount.java b/src/main/java/rx/internal/operators/OnSubscribeRefCount.java index 300dc8f104..22cbc9062d 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRefCount.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRefCount.java @@ -80,11 +80,8 @@ public void call(final Subscriber subscriber) { } } else { try { - // handle unsubscribing from the base subscription - subscriber.add(disconnect()); - // ready to subscribe to source so do it - source.unsafeSubscribe(subscriber); + doSubscribe(subscriber, baseSubscription); } finally { // release the read lock lock.unlock(); @@ -101,12 +98,8 @@ public void call(Subscription subscription) { try { baseSubscription.add(subscription); - - // handle unsubscribing from the base subscription - subscriber.add(disconnect()); - // ready to subscribe to source so do it - source.unsafeSubscribe(subscriber); + doSubscribe(subscriber, baseSubscription); } finally { // release the write lock lock.unlock(); @@ -115,18 +108,54 @@ public void call(Subscription subscription) { } }; } + + void doSubscribe(final Subscriber subscriber, final CompositeSubscription currentBase) { + // handle unsubscribing from the base subscription + subscriber.add(disconnect(currentBase)); + + source.unsafeSubscribe(new Subscriber(subscriber) { + @Override + public void onError(Throwable e) { + cleanup(); + subscriber.onError(e); + } + @Override + public void onNext(T t) { + subscriber.onNext(t); + } + @Override + public void onCompleted() { + cleanup(); + subscriber.onCompleted(); + } + void cleanup() { + lock.lock(); + try { + if (baseSubscription == currentBase) { + baseSubscription.unsubscribe(); + baseSubscription = new CompositeSubscription(); + subscriptionCount.set(0); + } + } finally { + lock.unlock(); + } + } + }); + } - private Subscription disconnect() { + private Subscription disconnect(final CompositeSubscription current) { return Subscriptions.create(new Action0() { @Override public void call() { lock.lock(); try { - if (subscriptionCount.decrementAndGet() == 0) { - baseSubscription.unsubscribe(); - // need a new baseSubscription because once - // unsubscribed stays that way - baseSubscription = new CompositeSubscription(); + if (baseSubscription == current) { + if (subscriptionCount.decrementAndGet() == 0) { + baseSubscription.unsubscribe(); + // need a new baseSubscription because once + // unsubscribed stays that way + baseSubscription = new CompositeSubscription(); + } } } finally { lock.unlock(); diff --git a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java index c70fa6dbf1..1f500e289f 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java @@ -15,39 +15,24 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; 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.verify; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -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.AtomicInteger; -import org.junit.Before; -import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.MockitoAnnotations; +import org.junit.*; +import org.mockito.*; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; +import rx.Observable; import rx.Observer; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func2; -import rx.observers.Subscribers; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.schedulers.TestScheduler; +import rx.functions.*; +import rx.observers.*; +import rx.schedulers.*; import rx.subjects.ReplaySubject; import rx.subscriptions.Subscriptions; @@ -532,4 +517,72 @@ public Integer call(Integer t1, Integer t2) { ts2.assertReceivedOnNext(Arrays.asList(30)); } + @Test(timeout = 10000) + public void testUpstreamErrorAllowsRetry() throws InterruptedException { + final AtomicInteger intervalSubscribed = new AtomicInteger(); + Observable interval = + Observable.interval(200,TimeUnit.MILLISECONDS) + .doOnSubscribe( + new Action0() { + @Override + public void call() { + System.out.println("Subscribing to interval " + intervalSubscribed.incrementAndGet()); + } + } + ) + .flatMap(new Func1>() { + @Override + public Observable call(Long t1) { + return Observable.defer(new Func0>() { + @Override + public Observable call() { + return Observable.error(new Exception("Some exception")); + } + }); + } + }) + .onErrorResumeNext(new Func1>() { + @Override + public Observable call(Throwable t1) { + return Observable.error(t1); + } + }) + .publish() + .refCount(); + + interval + .doOnError(new Action1() { + @Override + public void call(Throwable t1) { + System.out.println("Subscriber 1 onError: " + t1); + } + }) + .retry(5) + .subscribe(new Action1() { + @Override + public void call(String t1) { + System.out.println("Subscriber 1: " + t1); + } + }); + Thread.sleep(100); + interval + .doOnError(new Action1() { + @Override + public void call(Throwable t1) { + System.out.println("Subscriber 2 onError: " + t1); + } + }) + .retry(5) + .subscribe(new Action1() { + @Override + public void call(String t1) { + System.out.println("Subscriber 2: " + t1); + } + }); + + Thread.sleep(1300); + + System.out.println(intervalSubscribed.get()); + assertEquals(6, intervalSubscribed.get()); + } } From 285ed87f177a9006330c59461ae962f811aae613 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 30 Jan 2015 10:53:57 +0100 Subject: [PATCH 101/857] Give more time to certain concurrency tests. --- src/test/java/rx/internal/operators/OperatorRetryTest.java | 4 ++-- .../java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java | 2 +- src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index 32a3e8f8ed..4be71c834d 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -682,7 +682,7 @@ public void testTimeoutWithRetry() { assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); } - @Test(timeout = 10000) + @Test(timeout = 15000) public void testRetryWithBackpressure() throws InterruptedException { final int NUM_RETRIES = RxRingBuffer.SIZE * 2; for (int i = 0; i < 400; i++) { @@ -705,7 +705,7 @@ public void testRetryWithBackpressure() throws InterruptedException { inOrder.verifyNoMoreInteractions(); } } - @Test(timeout = 10000) + @Test(timeout = 15000) public void testRetryWithBackpressureParallel() throws InterruptedException { final int NUM_RETRIES = RxRingBuffer.SIZE * 2; int ncpu = Runtime.getRuntime().availableProcessors(); diff --git a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java index 9bc3e47823..5c326cdd92 100644 --- a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java @@ -503,7 +503,7 @@ public void run() { t.join(); } - @Test(timeout = 5000) + @Test(timeout = 10000) public void testConcurrentSizeAndHasAnyValueTimeBounded() throws InterruptedException { final ReplaySubject rs = ReplaySubject.createWithTime(1, TimeUnit.MILLISECONDS, Schedulers.computation()); final CyclicBarrier cb = new CyclicBarrier(2); diff --git a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java index cc99eb737d..a90c5ac0f3 100644 --- a/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectConcurrencyTest.java @@ -407,7 +407,7 @@ public void call() { worker.unsubscribe(); } } - @Test(timeout = 5000) + @Test(timeout = 10000) public void testConcurrentSizeAndHasAnyValue() throws InterruptedException { final ReplaySubject rs = ReplaySubject.create(); final CyclicBarrier cb = new CyclicBarrier(2); From 84965829df9d24a65c4e4afce473fdec3efdf4c3 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 29 Jan 2015 17:35:00 +1100 Subject: [PATCH 102/857] add request overflow check for OnSubscribeFromIterable --- src/main/java/rx/Producer.java | 10 +++ src/main/java/rx/Subscriber.java | 12 ++- .../internal/operators/BackpressureUtils.java | 78 +++++++++++++++++++ .../operators/OnSubscribeFromIterable.java | 2 +- .../rx/internal/operators/OperatorMerge.java | 11 +-- .../OnSubscribeFromIterableTest.java | 36 +++++++++ 6 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 src/main/java/rx/internal/operators/BackpressureUtils.java diff --git a/src/main/java/rx/Producer.java b/src/main/java/rx/Producer.java index 0d4b6f8b37..37f88d1861 100644 --- a/src/main/java/rx/Producer.java +++ b/src/main/java/rx/Producer.java @@ -23,6 +23,16 @@ public interface Producer { /** * Request a certain maximum number of items from this Producer. This is a way of requesting backpressure. * To disable backpressure, pass {@code Long.MAX_VALUE} to this method. + *

+ * Requests are additive but if a sequence of requests totals more than {@code Long.MAX_VALUE} then + * {@code Long.MAX_VALUE} requests will be actioned and the extras may be ignored. Arriving at + * {@code Long.MAX_VALUE} by addition of requests cannot be assumed to disable backpressure. For example, + * the code below may result in {@code Long.MAX_VALUE} requests being actioned only. + * + *

+     * request(100);
+     * request(Long.MAX_VALUE-1);
+     * 
* * @param n the maximum number of items you want this Producer to produce, or {@code Long.MAX_VALUE} if you * want the Producer to produce items at its own pace diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 237817e88a..5522e3c326 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -89,7 +89,17 @@ public void onStart() { * Request a certain maximum number of emitted items from the Observable this Subscriber is subscribed to. * This is a way of requesting backpressure. To disable backpressure, pass {@code Long.MAX_VALUE} to this * method. - * + *

+ * Requests are additive but if a sequence of requests totals more than {@code Long.MAX_VALUE} then + * {@code Long.MAX_VALUE} requests will be actioned and the extras may be ignored. Arriving at + * {@code Long.MAX_VALUE} by addition of requests cannot be assumed to disable backpressure. For example, + * the code below may result in {@code Long.MAX_VALUE} requests being actioned only. + * + *

+     * request(100);
+     * request(Long.MAX_VALUE-1);
+     * 
+ * * @param n the maximum number of items you want the Observable to emit to the Subscriber at this time, or * {@code Long.MAX_VALUE} if you want the Observable to emit items at its own pace * @throws IllegalArgumentException diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java new file mode 100644 index 0000000000..7e7ce2ae55 --- /dev/null +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -0,0 +1,78 @@ +/** + * Copyright 2015 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.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +/** + * Utility functions for use with backpressure. + * + */ +final class BackpressureUtils { + + /** + * Adds {@code n} to {@code requested} field and returns the value prior to + * addition once the addition is successful (uses CAS semantics). If + * overflows then sets {@code requested} field to {@code Long.MAX_VALUE}. + * + * @param requested + * atomic field updater for a request count + * @param object + * contains the field updated by the updater + * @param n + * the number of requests to add to the requested count + * @return requested value just prior to successful addition + */ + static long getAndAddRequest(AtomicLongFieldUpdater requested, T object, long n) { + // add n to field but check for overflow + while (true) { + long current = requested.get(object); + long next = current + n; + // check for overflow + if (next < 0) + next = Long.MAX_VALUE; + if (requested.compareAndSet(object, current, next)) + return current; + } + } + + /** + * Adds {@code n} to {@code requested} and returns the value prior to addition once the + * addition is successful (uses CAS semantics). If overflows then sets + * {@code requested} field to {@code Long.MAX_VALUE}. + * + * @param requested + * atomic field updater for a request count + * @param object + * contains the field updated by the updater + * @param n + * the number of requests to add to the requested count + * @return requested value just prior to successful addition + */ + static long getAndAddRequest(AtomicLong requested, long n) { + // add n to field but check for overflow + while (true) { + long current = requested.get(); + long next = current + n; + // check for overflow + if (next < 0) + next = Long.MAX_VALUE; + if (requested.compareAndSet(current, next)) + return current; + } + } +} diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index 500f5a34a7..4a27013b89 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -80,7 +80,7 @@ public void request(long n) { } } else if(n > 0) { // backpressure is requested - long _c = REQUESTED_UPDATER.getAndAdd(this, n); + long _c = BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER, this, n); if (_c == 0) { while (true) { /* diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 384a7bea42..2da1844ca9 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -545,16 +545,7 @@ public void request(long n) { if (n == Long.MAX_VALUE) { requested = Long.MAX_VALUE; } else { - // add n to requested but check for overflow - while (true) { - long current = REQUESTED.get(this); - long next = current + n; - //check for overflow - if (next < 0) - next = Long.MAX_VALUE; - if (REQUESTED.compareAndSet(this, current, next)) - break; - } + BackpressureUtils.getAndAddRequest(REQUESTED, this, n); if (ms.drainQueuesIfNeeded()) { boolean sendComplete = false; synchronized (ms) { diff --git a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java index 68d0ed09e9..b9f829783a 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java @@ -15,6 +15,7 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -24,14 +25,18 @@ import java.util.Arrays; import java.util.Collections; import java.util.Iterator; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.junit.Test; import org.mockito.Mockito; import rx.Observable; import rx.Observer; +import rx.Subscriber; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; public class OnSubscribeFromIterableTest { @@ -157,5 +162,36 @@ public void testSubscribeMultipleTimes() { o.call(ts); ts.assertReceivedOnNext(Arrays.asList(1, 2, 3)); } + + @Test + public void testFromIterableRequestOverflow() throws InterruptedException { + Observable o = Observable.from(Arrays.asList(1,2,3,4)); + final int expectedCount = 4; + final CountDownLatch latch = new CountDownLatch(expectedCount); + o.subscribeOn(Schedulers.computation()).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onCompleted() { + //ignore + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer t) { + latch.countDown(); + request(Long.MAX_VALUE-1); + }}); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + } From 733cbc0e5d3e0206a6502d9fae1d7b6fe0efcd89 Mon Sep 17 00:00:00 2001 From: Jalandip Date: Fri, 30 Jan 2015 18:00:13 -0800 Subject: [PATCH 103/857] SizeEviction test needs to return false when verifying if it needs eviction --- src/main/java/rx/subjects/ReplaySubject.java | 2 +- .../operators/OperatorReplayTest.java | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index 73ddf8585a..badf09a948 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -838,7 +838,7 @@ public void evict(NodeList t1) { @Override public boolean test(Object value, long now) { - return true; // size gets never stale + return false; // size gets never stale } @Override diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 6a6e48f068..8e6dddce8c 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -87,6 +87,58 @@ public void testBufferedReplay() { } } + @Test + public void testBufferedWindowReplay() { + PublishSubject source = PublishSubject.create(); + TestScheduler scheduler = new TestScheduler(); + ConnectableObservable co = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler); + co.connect(); + + { + @SuppressWarnings("unchecked") + Observer observer1 = mock(Observer.class); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onNext(5); + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(4); + + inOrder.verify(observer1, times(1)).onNext(5); + + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + @SuppressWarnings("unchecked") + Observer observer1 = mock(Observer.class); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(5); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + @Test public void testWindowedReplay() { TestScheduler scheduler = new TestScheduler(); From c3ec2dc4e0d442838b4b57656bde4a4bb783914b Mon Sep 17 00:00:00 2001 From: George Campbell Date: Fri, 30 Jan 2015 17:05:22 -0800 Subject: [PATCH 104/857] subscribeOn drops the subscriptions returned from the scheduler. --- src/main/java/rx/internal/operators/OperatorSubscribeOn.java | 4 ++-- src/test/java/rx/internal/operators/OperatorReplayTest.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSubscribeOn.java b/src/main/java/rx/internal/operators/OperatorSubscribeOn.java index 152bc504e4..4489961241 100644 --- a/src/main/java/rx/internal/operators/OperatorSubscribeOn.java +++ b/src/main/java/rx/internal/operators/OperatorSubscribeOn.java @@ -54,7 +54,7 @@ public void onError(Throwable e) { @Override public void onNext(final Observable o) { - inner.schedule(new Action0() { + subscriber.add(inner.schedule(new Action0() { @Override public void call() { @@ -102,7 +102,7 @@ public void call() { }); } - }); + })); } }; diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 8e6dddce8c..0fb3e9ab88 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -609,6 +609,7 @@ public void testIssue2191_SchedulerUnsubscribe() throws Exception { verify(spiedWorker, times(1)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); + verify(mockSubscription, times(1)).unsubscribe(); verifyNoMoreInteractions(sourceNext); verifyNoMoreInteractions(sourceCompleted); @@ -668,6 +669,7 @@ public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { verify(spiedWorker, times(1)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); + verify(mockSubscription, times(1)).unsubscribe(); verifyNoMoreInteractions(sourceNext); verifyNoMoreInteractions(sourceCompleted); From 6cd17db782ffb3128764f929b3b80236080691f2 Mon Sep 17 00:00:00 2001 From: David Karnok Date: Sat, 31 Jan 2015 23:31:58 +0100 Subject: [PATCH 105/857] Revert "subscribeOn drops the subscriptions returned from the scheduler." --- src/main/java/rx/internal/operators/OperatorSubscribeOn.java | 4 ++-- src/test/java/rx/internal/operators/OperatorReplayTest.java | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSubscribeOn.java b/src/main/java/rx/internal/operators/OperatorSubscribeOn.java index 4489961241..152bc504e4 100644 --- a/src/main/java/rx/internal/operators/OperatorSubscribeOn.java +++ b/src/main/java/rx/internal/operators/OperatorSubscribeOn.java @@ -54,7 +54,7 @@ public void onError(Throwable e) { @Override public void onNext(final Observable o) { - subscriber.add(inner.schedule(new Action0() { + inner.schedule(new Action0() { @Override public void call() { @@ -102,7 +102,7 @@ public void call() { }); } - })); + }); } }; diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 0fb3e9ab88..8e6dddce8c 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -609,7 +609,6 @@ public void testIssue2191_SchedulerUnsubscribe() throws Exception { verify(spiedWorker, times(1)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); - verify(mockSubscription, times(1)).unsubscribe(); verifyNoMoreInteractions(sourceNext); verifyNoMoreInteractions(sourceCompleted); @@ -669,7 +668,6 @@ public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { verify(spiedWorker, times(1)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); - verify(mockSubscription, times(1)).unsubscribe(); verifyNoMoreInteractions(sourceNext); verifyNoMoreInteractions(sourceCompleted); From 787fbff3da9afa1e2d7c1b44c74a47ad39baf7fd Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 2 Feb 2015 20:25:44 +0100 Subject: [PATCH 106/857] Allow configuring maximum number of computation scheduler threads --- .../rx/schedulers/EventLoopsScheduler.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/schedulers/EventLoopsScheduler.java b/src/main/java/rx/schedulers/EventLoopsScheduler.java index 004fbeea78..22fc9c6d4d 100644 --- a/src/main/java/rx/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/schedulers/EventLoopsScheduler.java @@ -31,7 +31,24 @@ /** Manages a fixed number of workers. */ private static final String THREAD_NAME_PREFIX = "RxComputationThreadPool-"; private static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); - + /** + * Key to setting the maximum number of computation scheduler threads. + * Zero or less is interpreted as use available. Capped by available. + */ + static final String KEY_MAX_THREADS = "io.reactivex.rxjava.scheduler.max-computation-threads"; + /** The maximum number of computation scheduler threads. */ + static final int MAX_THREADS; + static { + int maxThreads = Integer.getInteger(KEY_MAX_THREADS, 0); + int ncpu = Runtime.getRuntime().availableProcessors(); + int max; + if (maxThreads <= 0 || maxThreads > ncpu) { + max = ncpu; + } else { + max =maxThreads; + } + MAX_THREADS = max; + } static final class FixedSchedulerPool { final int cores; @@ -40,7 +57,7 @@ static final class FixedSchedulerPool { FixedSchedulerPool() { // initialize event loops - this.cores = Runtime.getRuntime().availableProcessors(); + this.cores = MAX_THREADS; this.eventLoops = new PoolWorker[cores]; for (int i = 0; i < cores; i++) { this.eventLoops[i] = new PoolWorker(THREAD_FACTORY); From 7ab07ca1273a819e9a141b2616e0d3c861507b03 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 2 Feb 2015 20:26:39 +0100 Subject: [PATCH 107/857] Fixed typo. --- src/main/java/rx/schedulers/EventLoopsScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/schedulers/EventLoopsScheduler.java b/src/main/java/rx/schedulers/EventLoopsScheduler.java index 22fc9c6d4d..7881643a79 100644 --- a/src/main/java/rx/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/schedulers/EventLoopsScheduler.java @@ -45,7 +45,7 @@ if (maxThreads <= 0 || maxThreads > ncpu) { max = ncpu; } else { - max =maxThreads; + max = maxThreads; } MAX_THREADS = max; } From aa451ef114afc04493c1817352968775edda86f0 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 2 Feb 2015 22:41:53 +0100 Subject: [PATCH 108/857] Fixed race in testConnectUnsubscribeRaceCondition() --- .../internal/operators/OnSubscribeRefCountTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java index 1f500e289f..59d1ee7de4 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java @@ -270,6 +270,13 @@ public void call() { s.assertNoErrors(); } + @Test + public void testConnectUnsubscribeRaceConditionLoop() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + testConnectUnsubscribeRaceCondition(); + } + } + @Test public void testConnectUnsubscribeRaceCondition() throws InterruptedException { final AtomicInteger subUnsubCount = new AtomicInteger(); @@ -295,12 +302,14 @@ public void call() { }); TestSubscriber s = new TestSubscriber(); - o.publish().refCount().subscribeOn(Schedulers.newThread()).subscribe(s); + + o.publish().refCount().subscribeOn(Schedulers.computation()).subscribe(s); System.out.println("send unsubscribe"); // now immediately unsubscribe while subscribeOn is racing to subscribe s.unsubscribe(); // this generally will mean it won't even subscribe as it is already unsubscribed by the time connect() gets scheduled - + // give time to the counter to update + Thread.sleep(1); // either we subscribed and then unsubscribed, or we didn't ever even subscribe assertEquals(0, subUnsubCount.get()); From 152167e803a616ff5a330f83a80bb142a42373bf Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 2 Feb 2015 23:40:03 +0100 Subject: [PATCH 109/857] Added perf tests for various container-like subscriptions --- .../rx/internal/util/SubscriptionList.java | 15 +- .../CompositeSubscriptionConcurrentPerf.java | 198 ++++++++++++++++++ .../CompositeSubscriptionPerf.java | 121 +++++++++++ .../MultipleAssignmentSubscriptionPerf.java | 91 ++++++++ .../subscriptions/SerialSubscriptionPerf.java | 91 ++++++++ .../SubscriptionListConcurrentPerf.java | 131 ++++++++++++ .../subscriptions/SubscriptionListPerf.java | 94 +++++++++ 7 files changed, 739 insertions(+), 2 deletions(-) create mode 100644 src/perf/java/rx/subscriptions/CompositeSubscriptionConcurrentPerf.java create mode 100644 src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java create mode 100644 src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.java create mode 100644 src/perf/java/rx/subscriptions/SerialSubscriptionPerf.java create mode 100644 src/perf/java/rx/subscriptions/SubscriptionListConcurrentPerf.java create mode 100644 src/perf/java/rx/subscriptions/SubscriptionListPerf.java diff --git a/src/main/java/rx/internal/util/SubscriptionList.java b/src/main/java/rx/internal/util/SubscriptionList.java index bb2e7f514d..23bddebee1 100644 --- a/src/main/java/rx/internal/util/SubscriptionList.java +++ b/src/main/java/rx/internal/util/SubscriptionList.java @@ -78,15 +78,17 @@ public void add(final Subscription s) { */ @Override public void unsubscribe() { + List list; synchronized (this) { if (unsubscribed) { return; } unsubscribed = true; + list = subscriptions; + subscriptions = null; } // we will only get here once - unsubscribeFromAll(subscriptions); - subscriptions = null; + unsubscribeFromAll(list); } private static void unsubscribeFromAll(Collection subscriptions) { @@ -119,4 +121,13 @@ private static void unsubscribeFromAll(Collection subscriptions) { } } } + /* perf support */ + public void clear() { + List list; + synchronized (this) { + list = subscriptions; + subscriptions = null; + } + unsubscribeFromAll(list); + } } diff --git a/src/perf/java/rx/subscriptions/CompositeSubscriptionConcurrentPerf.java b/src/perf/java/rx/subscriptions/CompositeSubscriptionConcurrentPerf.java new file mode 100644 index 0000000000..0c2eee5796 --- /dev/null +++ b/src/perf/java/rx/subscriptions/CompositeSubscriptionConcurrentPerf.java @@ -0,0 +1,198 @@ +/** + * 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.subscriptions; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Group; +import org.openjdk.jmh.annotations.GroupThreads; +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.annotations.Threads; + +import rx.Subscription; + +/** + * Benchmark typical composite subscription concurrent behavior. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*CompositeSubscriptionConcurrentPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Threads(2) +@State(Scope.Group) +public class CompositeSubscriptionConcurrentPerf { + @Param({ "1", "1000", "100000" }) + public int loop; + + public final CompositeSubscription csub = new CompositeSubscription(); + @Param({ "1", "5", "10", "20" }) + public int count; + + public Subscription[] values; + @Setup + public void setup() { + values = new Subscription[count * 2]; + for (int i = 0; i < count * 2; i++) { + values[i] = new Subscription() { + @Override + public boolean isUnsubscribed() { + return false; + } + @Override + public void unsubscribe() { + + } + }; + } + } + + @Group("g1") + @GroupThreads(1) + @Benchmark + public void addRemoveT1() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + + for (int i = loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(values[j]); + } + for (int j = values.length - 1; j >= 0; j--) { + csub.remove(values[j]); + } + } + } + @Group("g1") + @GroupThreads(1) + @Benchmark + public void addRemoveT2() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + + for (int i = loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(values[j]); + } + for (int j = values.length - 1; j >= 0; j--) { + csub.remove(values[j]); + } + } + } + @Group("g2") + @GroupThreads(1) + @Benchmark + public void addRemoveHalfT1() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + int n = values.length; + + for (int i = loop; i > 0; i--) { + for (int j = n / 2 - 1; j >= 0; j--) { + csub.add(values[j]); + } + for (int j = n / 2 - 1; j >= 0; j--) { + csub.remove(values[j]); + } + } + } + @Group("g2") + @GroupThreads(1) + @Benchmark + public void addRemoveHalfT2() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + int n = values.length; + + for (int i = loop; i > 0; i--) { + for (int j = n - 1; j >= n / 2; j--) { + csub.add(values[j]); + } + for (int j = n - 1; j >= n / 2; j--) { + csub.remove(values[j]); + } + } + } + @Group("g3") + @GroupThreads(1) + @Benchmark + public void addClearT1() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + + for (int i = loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(values[j]); + } + csub.clear(); + } + } + @Group("g3") + @GroupThreads(1) + @Benchmark + public void addClearT2() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + + for (int i = loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(values[j]); + } + for (int j = values.length - 1; j >= 0; j--) { + csub.remove(values[j]); + } + } + } + @Group("g4") + @GroupThreads(1) + @Benchmark + public void addClearHalfT1() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + int n = values.length; + + for (int i = loop; i > 0; i--) { + for (int j = n / 2 - 1; j >= 0; j--) { + csub.add(values[j]); + } + csub.clear(); + } + } + @Group("g4") + @GroupThreads(1) + @Benchmark + public void addClearHalfT2() { + CompositeSubscription csub = this.csub; + Subscription[] values = this.values; + int n = values.length; + + for (int i = loop; i > 0; i--) { + for (int j = n - 1; j >= n / 2; j--) { + csub.add(values[j]); + } + csub.clear(); + } + } +} diff --git a/src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java b/src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java new file mode 100644 index 0000000000..6ff394d525 --- /dev/null +++ b/src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java @@ -0,0 +1,121 @@ +/** + * 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.subscriptions; + +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 rx.Subscription; + +/** + * Benchmark typical composite subscription single-threaded behavior. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*CompositeSubscriptionPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class CompositeSubscriptionPerf { + @State(Scope.Thread) + public static class TheState { + @Param({ "1", "1000", "100000" }) + public int loop; + @Param({ "1", "5", "10", "100" }) + public int count; + + public final CompositeSubscription csub = new CompositeSubscription(); + + public Subscription[] values; + @Setup + public void setup() { + values = new Subscription[count]; + for (int i = 0; i < count; i++) { + values[i] = new Subscription() { + @Override + public boolean isUnsubscribed() { + return false; + } + @Override + public void unsubscribe() { + + } + }; + } + } + } + @Benchmark + public void addRemove(TheState state) { + CompositeSubscription csub = state.csub; + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(state.values[j]); + } + for (int j = values.length - 1; j >= 0; j--) { + csub.remove(state.values[j]); + } + } + } + @Benchmark + public void addRemoveLocal(TheState state) { + CompositeSubscription csub = new CompositeSubscription(); + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(state.values[j]); + } + for (int j = values.length - 1; j >= 0; j--) { + csub.remove(state.values[j]); + } + } + } + @Benchmark + public void addClear(TheState state) { + CompositeSubscription csub = state.csub; + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(state.values[j]); + } + csub.clear(); + } + } + @Benchmark + public void addClearLocal(TheState state) { + CompositeSubscription csub = new CompositeSubscription(); + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(state.values[j]); + } + csub.clear(); + } + } +} diff --git a/src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.java b/src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.java new file mode 100644 index 0000000000..1623c7d781 --- /dev/null +++ b/src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.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.subscriptions; + +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 rx.Subscription; + +/** + * Benchmark typical multiple-assignment subscription behavior. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*MultipleAssignmentSubscriptionPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class MultipleAssignmentSubscriptionPerf { + @State(Scope.Thread) + public static class TheState { + @Param({ "1", "1000", "100000" }) + public int loop; + @Param({ "1", "5", "10", "100" }) + public int count; + + public final MultipleAssignmentSubscription csub = new MultipleAssignmentSubscription(); + + public Subscription[] values; + @Setup + public void setup() { + values = new Subscription[count]; + for (int i = 0; i < count; i++) { + values[i] = new Subscription() { + @Override + public boolean isUnsubscribed() { + return false; + } + @Override + public void unsubscribe() { + + } + }; + } + } + } + @Benchmark + public void add(TheState state) { + MultipleAssignmentSubscription csub = state.csub; + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.set(state.values[j]); + } + } + } + @Benchmark + public void addLocal(TheState state) { + MultipleAssignmentSubscription csub = new MultipleAssignmentSubscription(); + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.set(state.values[j]); + } + } + } +} diff --git a/src/perf/java/rx/subscriptions/SerialSubscriptionPerf.java b/src/perf/java/rx/subscriptions/SerialSubscriptionPerf.java new file mode 100644 index 0000000000..a8103d917e --- /dev/null +++ b/src/perf/java/rx/subscriptions/SerialSubscriptionPerf.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.subscriptions; + +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 rx.Subscription; + +/** + * Benchmark typical serial subscription behavior. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*SerialSubscriptionPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class SerialSubscriptionPerf { + @State(Scope.Thread) + public static class TheState { + @Param({ "1", "1000", "100000" }) + public int loop; + @Param({ "1", "5", "10", "100" }) + public int count; + + public final SerialSubscription csub = new SerialSubscription(); + + public Subscription[] values; + @Setup + public void setup() { + values = new Subscription[count]; + for (int i = 0; i < count; i++) { + values[i] = new Subscription() { + @Override + public boolean isUnsubscribed() { + return false; + } + @Override + public void unsubscribe() { + + } + }; + } + } + } + @Benchmark + public void add(TheState state) { + SerialSubscription csub = state.csub; + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.set(state.values[j]); + } + } + } + @Benchmark + public void addLocal(TheState state) { + SerialSubscription csub = new SerialSubscription(); + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.set(state.values[j]); + } + } + } +} diff --git a/src/perf/java/rx/subscriptions/SubscriptionListConcurrentPerf.java b/src/perf/java/rx/subscriptions/SubscriptionListConcurrentPerf.java new file mode 100644 index 0000000000..93c7438f2c --- /dev/null +++ b/src/perf/java/rx/subscriptions/SubscriptionListConcurrentPerf.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.subscriptions; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Group; +import org.openjdk.jmh.annotations.GroupThreads; +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.annotations.Threads; + +import rx.Subscription; +import rx.internal.util.SubscriptionList; + +/** + * Benchmark typical subscription list concurrent behavior. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*CompositeSubscriptionConcurrentPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@Threads(2) +@State(Scope.Group) +public class SubscriptionListConcurrentPerf { + @Param({ "1", "1000", "100000" }) + public int loop; + + public final SubscriptionList csub = new SubscriptionList(); + @Param({ "1", "5", "10", "20" }) + public int count; + + public Subscription[] values; + @Setup + public void setup() { + values = new Subscription[count * 2]; + for (int i = 0; i < count * 2; i++) { + values[i] = new Subscription() { + @Override + public boolean isUnsubscribed() { + return false; + } + @Override + public void unsubscribe() { + + } + }; + } + } + + @Group("g1") + @GroupThreads(1) + @Benchmark + public void addClearT1() { + SubscriptionList csub = this.csub; + Subscription[] values = this.values; + + for (int i = loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(values[j]); + } + csub.clear(); + } + } + @Group("g1") + @GroupThreads(1) + @Benchmark + public void addClearT2() { + SubscriptionList csub = this.csub; + Subscription[] values = this.values; + + for (int i = loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(values[j]); + } + csub.clear(); + } + } + @Group("g2") + @GroupThreads(1) + @Benchmark + public void addClearHalfT1() { + SubscriptionList csub = this.csub; + Subscription[] values = this.values; + int n = values.length; + + for (int i = loop; i > 0; i--) { + for (int j = n / 2 - 1; j >= 0; j--) { + csub.add(values[j]); + } + csub.clear(); + } + } + @Group("g2") + @GroupThreads(1) + @Benchmark + public void addRemoveHalfT2() { + SubscriptionList csub = this.csub; + Subscription[] values = this.values; + int n = values.length; + + for (int i = loop; i > 0; i--) { + for (int j = n - 1; j >= n / 2; j--) { + csub.add(values[j]); + } + csub.clear(); + } + } +} diff --git a/src/perf/java/rx/subscriptions/SubscriptionListPerf.java b/src/perf/java/rx/subscriptions/SubscriptionListPerf.java new file mode 100644 index 0000000000..02b7b467d2 --- /dev/null +++ b/src/perf/java/rx/subscriptions/SubscriptionListPerf.java @@ -0,0 +1,94 @@ +/** + * 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.subscriptions; + +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 rx.Subscription; +import rx.internal.util.SubscriptionList; + +/** + * Benchmark typical subscription list behavior. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*SubscriptionListPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class SubscriptionListPerf { + @State(Scope.Thread) + public static class TheState { + @Param({ "1", "1000", "100000" }) + public int loop; + @Param({ "1", "5", "10", "100" }) + public int count; + + public final SubscriptionList csub = new SubscriptionList(); + + public Subscription[] values; + @Setup + public void setup() { + values = new Subscription[count]; + for (int i = 0; i < count; i++) { + values[i] = new Subscription() { + @Override + public boolean isUnsubscribed() { + return false; + } + @Override + public void unsubscribe() { + + } + }; + } + } + } + @Benchmark + public void addClear(TheState state) { + SubscriptionList csub = state.csub; + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(state.values[j]); + } + csub.clear(); + } + } + @Benchmark + public void addClearLocal(TheState state) { + SubscriptionList csub = new SubscriptionList(); + Subscription[] values = state.values; + + for (int i = state.loop; i > 0; i--) { + for (int j = values.length - 1; j >= 0; j--) { + csub.add(state.values[j]); + } + csub.clear(); + } + } +} From 7163288b11a67ddf6a155b7af4916d6b206a688e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 08:28:16 +0100 Subject: [PATCH 110/857] Fixed local variables, added blackhole to *Local benchmarks --- .../CompositeSubscriptionPerf.java | 29 +++++++++---------- .../MultipleAssignmentSubscriptionPerf.java | 17 ++++------- .../subscriptions/SerialSubscriptionPerf.java | 17 ++++------- .../subscriptions/SubscriptionListPerf.java | 17 ++++------- 4 files changed, 31 insertions(+), 49 deletions(-) diff --git a/src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java b/src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java index 6ff394d525..bbbd8f1b63 100644 --- a/src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java +++ b/src/perf/java/rx/subscriptions/CompositeSubscriptionPerf.java @@ -18,14 +18,8 @@ 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.annotations.*; +import org.openjdk.jmh.infra.Blackhole; import rx.Subscription; @@ -73,26 +67,28 @@ public void addRemove(TheState state) { for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.add(state.values[j]); + csub.add(values[j]); } for (int j = values.length - 1; j >= 0; j--) { - csub.remove(state.values[j]); + csub.remove(values[j]); } } } @Benchmark - public void addRemoveLocal(TheState state) { + public void addRemoveLocal(TheState state, Blackhole bh) { CompositeSubscription csub = new CompositeSubscription(); Subscription[] values = state.values; for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.add(state.values[j]); + csub.add(values[j]); } for (int j = values.length - 1; j >= 0; j--) { - csub.remove(state.values[j]); + csub.remove(values[j]); } } + + bh.consume(csub); } @Benchmark public void addClear(TheState state) { @@ -101,21 +97,22 @@ public void addClear(TheState state) { for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.add(state.values[j]); + csub.add(values[j]); } csub.clear(); } } @Benchmark - public void addClearLocal(TheState state) { + public void addClearLocal(TheState state, Blackhole bh) { CompositeSubscription csub = new CompositeSubscription(); Subscription[] values = state.values; for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.add(state.values[j]); + csub.add(values[j]); } csub.clear(); } + bh.consume(csub); } } diff --git a/src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.java b/src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.java index 1623c7d781..b45a1c4c83 100644 --- a/src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.java +++ b/src/perf/java/rx/subscriptions/MultipleAssignmentSubscriptionPerf.java @@ -18,14 +18,8 @@ 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.annotations.*; +import org.openjdk.jmh.infra.Blackhole; import rx.Subscription; @@ -73,19 +67,20 @@ public void add(TheState state) { for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.set(state.values[j]); + csub.set(values[j]); } } } @Benchmark - public void addLocal(TheState state) { + public void addLocal(TheState state, Blackhole bh) { MultipleAssignmentSubscription csub = new MultipleAssignmentSubscription(); Subscription[] values = state.values; for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.set(state.values[j]); + csub.set(values[j]); } } + bh.consume(csub); } } diff --git a/src/perf/java/rx/subscriptions/SerialSubscriptionPerf.java b/src/perf/java/rx/subscriptions/SerialSubscriptionPerf.java index a8103d917e..5f6a275b00 100644 --- a/src/perf/java/rx/subscriptions/SerialSubscriptionPerf.java +++ b/src/perf/java/rx/subscriptions/SerialSubscriptionPerf.java @@ -18,14 +18,8 @@ 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.annotations.*; +import org.openjdk.jmh.infra.Blackhole; import rx.Subscription; @@ -73,19 +67,20 @@ public void add(TheState state) { for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.set(state.values[j]); + csub.set(values[j]); } } } @Benchmark - public void addLocal(TheState state) { + public void addLocal(TheState state, Blackhole bh) { SerialSubscription csub = new SerialSubscription(); Subscription[] values = state.values; for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.set(state.values[j]); + csub.set(values[j]); } } + bh.consume(csub); } } diff --git a/src/perf/java/rx/subscriptions/SubscriptionListPerf.java b/src/perf/java/rx/subscriptions/SubscriptionListPerf.java index 02b7b467d2..ca2240275e 100644 --- a/src/perf/java/rx/subscriptions/SubscriptionListPerf.java +++ b/src/perf/java/rx/subscriptions/SubscriptionListPerf.java @@ -18,14 +18,8 @@ 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.annotations.*; +import org.openjdk.jmh.infra.Blackhole; import rx.Subscription; import rx.internal.util.SubscriptionList; @@ -74,21 +68,22 @@ public void addClear(TheState state) { for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.add(state.values[j]); + csub.add(values[j]); } csub.clear(); } } @Benchmark - public void addClearLocal(TheState state) { + public void addClearLocal(TheState state, Blackhole bh) { SubscriptionList csub = new SubscriptionList(); Subscription[] values = state.values; for (int i = state.loop; i > 0; i--) { for (int j = values.length - 1; j >= 0; j--) { - csub.add(state.values[j]); + csub.add(values[j]); } csub.clear(); } + bh.consume(csub); } } From e0e630e050a75cf0137d335ac51ff137f3608b93 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 09:18:13 +0100 Subject: [PATCH 111/857] Changed predicate to stopPredicate: stops on becoming true. --- src/main/java/rx/Observable.java | 10 +++++----- .../operators/OperatorTakeUntilPredicate.java | 12 ++++++------ .../operators/OperatorTakeUntilPredicateTest.java | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 944ac11f84..84d3f6cf5c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7827,21 +7827,21 @@ public final Observable takeWhile(final Func1 predicate) /** * Returns an Observable that first emits items emitted by the source Observable, * checks the specified condition after each item, and - * then completes as soon as this condition is not satisfied. + * then completes if the condition is satisfied. *

* The difference between this operator and {@link #takeWhile(Func1)} is that here, the condition is evaluated after * the item was emitted. * - * @param predicate + * @param stopPredicate * a function that evaluates an item emitted by the source Observable and returns a Boolean * @return an Observable that first emits items emitted by the source Observable, * checks the specified condition after each item, and - * then completes as soon as this condition is not satisfied. + * then completes if the condition is satisfied. * @see Observable#takeWhile(Func1) */ @Experimental - public final Observable takeUntil(final Func1 predicate) { - return lift(new OperatorTakeUntilPredicate(predicate)); + public final Observable takeUntil(final Func1 stopPredicate) { + return lift(new OperatorTakeUntilPredicate(stopPredicate)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java index bb5eda0d24..5c3b2eae5c 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java @@ -40,16 +40,16 @@ private ParentSubscriber(Subscriber child) { public void onNext(T args) { child.onNext(args); - boolean doContinue = false; + boolean stop = false; try { - doContinue = predicate.call(args); + stop = stopPredicate.call(args); } catch (Throwable e) { done = true; child.onError(e); unsubscribe(); return; } - if (!doContinue) { + if (stop) { done = true; child.onCompleted(); unsubscribe(); @@ -74,10 +74,10 @@ void downstreamRequest(long n) { } } - private final Func1 predicate; + private final Func1 stopPredicate; - public OperatorTakeUntilPredicate(final Func1 predicate) { - this.predicate = predicate; + public OperatorTakeUntilPredicate(final Func1 stopPredicate) { + this.stopPredicate = stopPredicate; } @Override diff --git a/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java index f30e570693..f8e83f04ff 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java @@ -47,7 +47,7 @@ public void takeAll() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - Observable.just(1, 2).takeUntil(UtilityFunctions.alwaysTrue()).subscribe(o); + Observable.just(1, 2).takeUntil(UtilityFunctions.alwaysFalse()).subscribe(o); verify(o).onNext(1); verify(o).onNext(2); @@ -59,7 +59,7 @@ public void takeFirst() { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - Observable.just(1, 2).takeUntil(UtilityFunctions.alwaysFalse()).subscribe(o); + Observable.just(1, 2).takeUntil(UtilityFunctions.alwaysTrue()).subscribe(o); verify(o).onNext(1); verify(o, never()).onNext(2); @@ -74,7 +74,7 @@ public void takeSome() { Observable.just(1, 2, 3).takeUntil(new Func1() { @Override public Boolean call(Integer t1) { - return t1 < 2; + return t1 == 2; } }).subscribe(o); @@ -110,7 +110,7 @@ public void sourceThrows() { Observable.just(1) .concatWith(Observable.error(new TestException())) .concatWith(Observable.just(2)) - .takeUntil(UtilityFunctions.alwaysTrue()).subscribe(o); + .takeUntil(UtilityFunctions.alwaysFalse()).subscribe(o); verify(o).onNext(1); verify(o, never()).onNext(2); @@ -126,7 +126,7 @@ public void onStart() { } }; - Observable.range(1, 1000).takeUntil(UtilityFunctions.alwaysTrue()).subscribe(ts); + Observable.range(1, 1000).takeUntil(UtilityFunctions.alwaysFalse()).subscribe(ts); ts.assertNoErrors(); ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); From 39916953473c780a4d38982d77742e3d8cc1b6ee Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 09:35:25 +0100 Subject: [PATCH 112/857] Fixed some concerns with the operator. --- .../operators/OperatorSwitchIfEmpty.java | 20 +++---- .../operators/OperatorSwitchIfEmptyTest.java | 53 +++++++++---------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java index 615594cc17..ad0c14e8df 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java +++ b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java @@ -15,12 +15,11 @@ */ package rx.internal.operators; -import rx.Observable; -import rx.Producer; -import rx.Subscriber; - import java.util.concurrent.atomic.AtomicLong; +import rx.*; +import rx.subscriptions.SerialSubscription; + /** * If the Observable completes without emitting any items, subscribe to an alternate Observable. Allows for similar * functionality to {@link rx.internal.operators.OperatorDefaultIfEmpty} except instead of one item being emitted when @@ -35,8 +34,10 @@ public OperatorSwitchIfEmpty(Observable alternate) { @Override public Subscriber call(Subscriber child) { - final SwitchIfEmptySubscriber parent = new SwitchIfEmptySubscriber(child); - child.add(parent); + final SerialSubscription ssub = new SerialSubscription(); + final SwitchIfEmptySubscriber parent = new SwitchIfEmptySubscriber(child, ssub); + ssub.set(parent); + child.add(ssub); return parent; } @@ -46,9 +47,11 @@ private class SwitchIfEmptySubscriber extends Subscriber { final AtomicLong consumerCapacity = new AtomicLong(0l); private final Subscriber child; + final SerialSubscription ssub; - public SwitchIfEmptySubscriber(Subscriber child) { + public SwitchIfEmptySubscriber(Subscriber child, final SerialSubscription ssub) { this.child = child; + this.ssub = ssub; } @Override @@ -69,13 +72,12 @@ public void onCompleted() { if (!empty) { child.onCompleted(); } else if (!child.isUnsubscribed()) { - unsubscribe(); subscribeToAlternate(); } } private void subscribeToAlternate() { - child.add(alternate.unsafeSubscribe(new Subscriber() { + ssub.set(alternate.unsafeSubscribe(new Subscriber() { @Override public void setProducer(final Producer producer) { diff --git a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java index 3fc735ccab..ce52bccd6f 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java @@ -15,23 +15,19 @@ */ package rx.internal.operators; +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.Test; + +import rx.*; import rx.Observable; -import rx.Producer; -import rx.Subscriber; -import rx.Subscription; import rx.functions.Action0; +import rx.observers.TestSubscriber; import rx.subscriptions.Subscriptions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - public class OperatorSwitchIfEmptyTest { @Test @@ -134,30 +130,31 @@ public void call(final Subscriber subscriber) { @Test public void testSwitchRequestAlternativeObservableWithBackpressure() { - final List items = new ArrayList(); - Observable.empty().switchIfEmpty(Observable.just(1, 2, 3)).subscribe(new Subscriber() { + TestSubscriber ts = new TestSubscriber() { @Override public void onStart() { request(1); } + }; + Observable.empty().switchIfEmpty(Observable.just(1, 2, 3)).subscribe(ts); + + assertEquals(Arrays.asList(1), ts.getOnNextEvents()); + ts.assertNoErrors(); + } + @Test + public void testBackpressureNoRequest() { + TestSubscriber ts = new TestSubscriber() { @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onNext(Integer integer) { - items.add(integer); + public void onStart() { + request(0); } - }); - assertEquals(Arrays.asList(1), items); + }; + Observable.empty().switchIfEmpty(Observable.just(1, 2, 3)).subscribe(ts); + + assertTrue(ts.getOnNextEvents().isEmpty()); + ts.assertNoErrors(); } } \ No newline at end of file From c4f3ac396a3570b0e998e53bfe2510ea28726e00 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 09:44:27 +0100 Subject: [PATCH 113/857] Using @Experimental, covariance and final class --- src/main/java/rx/Observable.java | 4 ++-- .../java/rx/internal/operators/OperatorSwitchIfEmpty.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 5aac1e53c1..8b99ddd4e9 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3752,8 +3752,8 @@ public final Observable defaultIfEmpty(T defaultValue) { * @return an Observable that emits the items emitted by the source Observable or the items of an alternate Observable if the source Observable * is empty. */ - @Beta - public final Observable switchIfEmpty(Observable alternate) { + @Experimental + public final Observable switchIfEmpty(Observable alternate) { return lift(new OperatorSwitchIfEmpty(alternate)); } diff --git a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java index ad0c14e8df..4b106f9be5 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java +++ b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java @@ -25,10 +25,10 @@ * functionality to {@link rx.internal.operators.OperatorDefaultIfEmpty} except instead of one item being emitted when * empty, the results of the given Observable will be emitted. */ -public class OperatorSwitchIfEmpty implements Observable.Operator { - private final Observable alternate; +public final class OperatorSwitchIfEmpty implements Observable.Operator { + private final Observable alternate; - public OperatorSwitchIfEmpty(Observable alternate) { + public OperatorSwitchIfEmpty(Observable alternate) { this.alternate = alternate; } From e382e7ec62e8b87fe61403e5a0726c2f443d411e Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 3 Feb 2015 09:08:31 -0800 Subject: [PATCH 114/857] add @since annotation to new operator (also: remove "Beta" section as it's been moved from Beta to Experimental status) --- src/main/java/rx/Observable.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 1c27bb8df2..2e33ca4906 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3749,20 +3749,19 @@ public final Observable defaultIfEmpty(T defaultValue) { } /** - * Returns an Observable that emits the items emitted by the source Observable or the items of an alternate Observable if the source Observable - * is empty. + * Returns an Observable that emits the items emitted by the source Observable or the items of an alternate + * Observable if the source Observable is empty. *

*

*
Scheduler:
*
{@code switchIfEmpty} does not operate by default on a particular {@link Scheduler}.
- *
Beta:
- *
{@code switchIfEmpty} is currently in {@link rx.annotations.Beta} and subject to change.
*
* * @param alternate * the alternate Observable to subscribe to if the source does not emit any items - * @return an Observable that emits the items emitted by the source Observable or the items of an alternate Observable if the source Observable - * is empty. + * @return an Observable that emits the items emitted by the source Observable or the items of an + * alternate Observable if the source Observable is empty. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public final Observable switchIfEmpty(Observable alternate) { From a2804809cd06e9aaa593a33fbc02aaf021852100 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 20:49:18 +0100 Subject: [PATCH 115/857] Repeat/retry: fixed unbounded downstream requesting above Long.MAX_VALUE --- .../internal/operators/OnSubscribeRedo.java | 2 +- .../operators/OperatorRepeatTest.java | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 946dcaec06..65fcb3eb92 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -323,7 +323,7 @@ public void setProducer(Producer producer) { @Override public void request(final long n) { - long c = consumerCapacity.getAndAdd(n); + long c = BackpressureUtils.getAndAddRequest(consumerCapacity, n); Producer producer = currentProducer.get(); if (producer != null) { producer.request(n); diff --git a/src/test/java/rx/internal/operators/OperatorRepeatTest.java b/src/test/java/rx/internal/operators/OperatorRepeatTest.java index b317c85672..d8653a14e6 100644 --- a/src/test/java/rx/internal/operators/OperatorRepeatTest.java +++ b/src/test/java/rx/internal/operators/OperatorRepeatTest.java @@ -18,11 +18,9 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; -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 static org.mockito.Mockito.*; +import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; @@ -33,6 +31,7 @@ import rx.Subscriber; import rx.exceptions.TestException; import rx.functions.Func1; +import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; public class OperatorRepeatTest { @@ -158,4 +157,21 @@ public void testRepeatOne() { verify(o, times(1)).onNext(any()); verify(o, never()).onError(any(Throwable.class)); } + + /** Issue #2587. */ + @Test + public void testRepeatAndDistinctUnbounded() { + Observable src = Observable.from(Arrays.asList(1, 2, 3, 4, 5)) + .take(3) + .repeat(3) + .distinct(); + + TestSubscriber ts = new TestSubscriber(); + + src.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3)); + } } From 01e97fc51c222cfabb033409ac021f8ce99b8147 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 21:03:31 +0100 Subject: [PATCH 116/857] Zip: fixed unbounded downstream requesting above Long.MAX_VALUE --- .../rx/internal/operators/OperatorZip.java | 7 ++++-- .../internal/operators/OperatorZipTest.java | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorZip.java b/src/main/java/rx/internal/operators/OperatorZip.java index 2c28eb0112..d7271478a7 100644 --- a/src/main/java/rx/internal/operators/OperatorZip.java +++ b/src/main/java/rx/internal/operators/OperatorZip.java @@ -117,6 +117,7 @@ public Subscriber call(final Subscriber child) return subscriber; } + @SuppressWarnings("rawtypes") private final class ZipSubscriber extends Subscriber { final Subscriber child; @@ -158,7 +159,8 @@ public void onNext(Observable[] observables) { } private static final class ZipProducer extends AtomicLong implements Producer { - + /** */ + private static final long serialVersionUID = -1216676403723546796L; private Zip zipper; public ZipProducer(Zip zipper) { @@ -167,7 +169,7 @@ public ZipProducer(Zip zipper) { @Override public void request(long n) { - addAndGet(n); + BackpressureUtils.getAndAddRequest(this, n); // try and claim emission if no other threads are doing so zipper.tick(); } @@ -179,6 +181,7 @@ private static final class Zip { private final FuncN zipFunction; private final CompositeSubscription childSubscription = new CompositeSubscription(); + @SuppressWarnings("unused") volatile long counter; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater COUNTER_UPDATER = AtomicLongFieldUpdater.newUpdater(Zip.class, "counter"); diff --git a/src/test/java/rx/internal/operators/OperatorZipTest.java b/src/test/java/rx/internal/operators/OperatorZipTest.java index ffcf9a769e..166be948f6 100644 --- a/src/test/java/rx/internal/operators/OperatorZipTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipTest.java @@ -1243,4 +1243,27 @@ public Integer call(Integer i1, Integer i2) { } assertEquals(expected, zip2.toList().toBlocking().single()); } + @Test + public void testUnboundedDownstreamOverrequesting() { + Observable source = Observable.range(1, 2).zipWith(Observable.range(1, 2), new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return t1 + 10 * t2; + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + super.onNext(t); + requestMore(5); + } + }; + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(11, 22)); + } } From 0951cdb0859fd4b55cf59df45aac18569dc04ded Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 21:30:41 +0100 Subject: [PATCH 117/857] Changed the naming of the NewThreadWorker's system parameters --- src/main/java/rx/internal/schedulers/NewThreadWorker.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index 65b8ac92a9..41144795cb 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -35,9 +35,9 @@ public class NewThreadWorker extends Scheduler.Worker implements Subscription { private final RxJavaSchedulersHook schedulersHook; volatile boolean isUnsubscribed; /** The purge frequency in milliseconds. */ - private static final String FREQUENCY_KEY = "io.reactivex.rxjava.scheduler.jdk6.purge-frequency-millis"; + private static final String FREQUENCY_KEY = "rx.scheduler.jdk6.purge-frequency-millis"; /** Force the use of purge (true/false). */ - private static final String PURGE_FORCE_KEY = "io.reactivex.rxjava.scheduler.jdk6.purge-force"; + private static final String PURGE_FORCE_KEY = "rx.scheduler.jdk6.purge-force"; private static final String PURGE_THREAD_PREFIX = "RxSchedulerPurge-"; /** Forces the use of purge even if setRemoveOnCancelPolicy is available. */ private static final boolean PURGE_FORCE; From d983d2b897b84a172c7eff0217713dd391a7b956 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 3 Feb 2015 21:45:39 +0100 Subject: [PATCH 118/857] Changed system-parameter naming. --- src/main/java/rx/schedulers/EventLoopsScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/schedulers/EventLoopsScheduler.java b/src/main/java/rx/schedulers/EventLoopsScheduler.java index 7881643a79..a674dfbc22 100644 --- a/src/main/java/rx/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/schedulers/EventLoopsScheduler.java @@ -35,7 +35,7 @@ * Key to setting the maximum number of computation scheduler threads. * Zero or less is interpreted as use available. Capped by available. */ - static final String KEY_MAX_THREADS = "io.reactivex.rxjava.scheduler.max-computation-threads"; + static final String KEY_MAX_THREADS = "rx.scheduler.max-computation-threads"; /** The maximum number of computation scheduler threads. */ static final int MAX_THREADS; static { From 802b41d5a83bc0b087bf2f7e2899298f19fee6cc Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Feb 2015 00:18:57 +0100 Subject: [PATCH 119/857] Zip: emit onCompleted without waiting for request + avoid re-reading fields --- .../rx/internal/operators/OperatorZip.java | 15 ++++--- .../internal/operators/OperatorZipTest.java | 41 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorZip.java b/src/main/java/rx/internal/operators/OperatorZip.java index d7271478a7..623731755a 100644 --- a/src/main/java/rx/internal/operators/OperatorZip.java +++ b/src/main/java/rx/internal/operators/OperatorZip.java @@ -223,17 +223,21 @@ public void start(@SuppressWarnings("rawtypes") Observable[] os, AtomicLong requ */ @SuppressWarnings("unchecked") void tick() { + final Object[] observers = this.observers; if (observers == null) { // nothing yet to do (initial request from Producer) return; } if (COUNTER_UPDATER.getAndIncrement(this) == 0) { + final int length = observers.length; + final Observer child = this.child; + final AtomicLong requested = this.requested; do { - // we only emit if requested > 0 - while (requested.get() > 0) { - final Object[] vs = new Object[observers.length]; + while (true) { + // peek for a potential onCompleted event + final Object[] vs = new Object[length]; boolean allHaveValues = true; - for (int i = 0; i < observers.length; i++) { + for (int i = 0; i < length; i++) { RxRingBuffer buffer = ((InnerSubscriber) observers[i]).items; Object n = buffer.peek(); @@ -252,7 +256,8 @@ void tick() { vs[i] = buffer.getValue(n); } } - if (allHaveValues) { + // we only emit if requested > 0 and have all values available + if (requested.get() > 0 && allHaveValues) { try { // all have something so emit child.onNext(zipFunction.call(vs)); diff --git a/src/test/java/rx/internal/operators/OperatorZipTest.java b/src/test/java/rx/internal/operators/OperatorZipTest.java index 166be948f6..3548b1da91 100644 --- a/src/test/java/rx/internal/operators/OperatorZipTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipTest.java @@ -36,6 +36,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import junit.framework.Assert; + import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; @@ -1266,4 +1268,43 @@ public void onNext(Integer t) { ts.assertTerminalEvent(); ts.assertReceivedOnNext(Arrays.asList(11, 22)); } + @Test(timeout = 10000) + public void testZipRace() { + Observable src = Observable.just(1).subscribeOn(Schedulers.computation()); + for (int i = 0; i < 500000; i++) { + int value = Observable.zip(src, src, new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return t1 + t2 * 10; + } + }).toBlocking().singleOrDefault(0); + + Assert.assertEquals(11, value); + } + } + /** + * Request only a single value and don't wait for another request just + * to emit an onCompleted. + */ + @Test + public void testZipRequest1() { + Observable src = Observable.just(1).subscribeOn(Schedulers.computation()); + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + requestMore(1); + } + }; + + Observable.zip(src, src, new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return t1 + t2 * 10; + } + }).subscribe(ts); + + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(11)); + } } From 55d5ca56a31be5e500c0520351792a822554d1cf Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Feb 2015 00:28:22 +0100 Subject: [PATCH 120/857] Less iteration. --- src/test/java/rx/internal/operators/OperatorZipTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/rx/internal/operators/OperatorZipTest.java b/src/test/java/rx/internal/operators/OperatorZipTest.java index 3548b1da91..8c7d453e47 100644 --- a/src/test/java/rx/internal/operators/OperatorZipTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipTest.java @@ -1271,7 +1271,7 @@ public void onNext(Integer t) { @Test(timeout = 10000) public void testZipRace() { Observable src = Observable.just(1).subscribeOn(Schedulers.computation()); - for (int i = 0; i < 500000; i++) { + for (int i = 0; i < 100000; i++) { int value = Observable.zip(src, src, new Func2() { @Override public Integer call(Integer t1, Integer t2) { From 76c80a862688a6a869e1db3a5db374d2890ccb27 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 3 Feb 2015 21:59:15 -0800 Subject: [PATCH 121/857] Revert Behavior Change in TestSubscriber.awaitTerminalEvent Reverts change made at https://github.com/ReactiveX/RxJava/pull/2332/files#diff-fbed6a16f49022fd2b10f45fd6dd015bR230 See discussion at https://github.com/ReactiveX/RxJava/issues/2549#issuecomment-72783738 The Javadoc for this method clearly states that it will wait until completion or timeout. It does not say it will throw an exception on timeout, so we can not start throwing as that is a behavioral change. --- src/main/java/rx/observers/TestSubscriber.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index ff9842f883..709027ea97 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -226,9 +226,7 @@ public void awaitTerminalEvent() { */ public void awaitTerminalEvent(long timeout, TimeUnit unit) { try { - if (!latch.await(timeout, unit)) { - throw new RuntimeException(new TimeoutException()); - } + latch.await(timeout, unit); } catch (InterruptedException e) { throw new RuntimeException("Interrupted", e); } From a2129d70e63b5740a25f8d7b0ab7148bc988138f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 3 Feb 2015 22:23:25 -0800 Subject: [PATCH 122/857] Version 1.0.5 --- CHANGES.md | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 28067fd11b..57d719efdb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,151 @@ # RxJava Releases # +### Version 1.0.5 – Feburary 3rd 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.5%7C)) ### + +This release includes many bug fixes along with a few new operators and enhancements. + +#### Experimental Operators + +This release adds a few experimental operators. + +Note that these APIs [may still change or be removed altogether](https://github.com/ReactiveX/RxJava#experimental) since they are marked as `@Experimental`. + +##### takeUntil(predicate) + +This operator allows conditionally unsubscribing an `Observable` but inclusively emitting the final `onNext`. This differs from `takeWhile` which excludes the final `onNext`. + +```java +// takeUntil(predicate) example +Observable.just(1, 2, 3, 4, 5, 6, 7) + .doOnEach(System.out::println) + .takeUntil(i -> i == 3) + .forEach(System.out::println); + +// takeWhile(predicate) example +Observable.just(1, 2, 3, 4, 5, 6, 7) + .doOnEach(System.out::println) + .takeWhile(i -> i <= 3) + .forEach(System.out::println); +``` + +This outputs: + +``` +// takeUntil(predicate) +[rx.Notification@30e84925 OnNext 1] +1 +[rx.Notification@30e84926 OnNext 2] +2 +[rx.Notification@30e84927 OnNext 3] +3 + +// takeWhile(predicate) +[rx.Notification@30e84925 OnNext 1] +1 +[rx.Notification@30e84926 OnNext 2] +2 +[rx.Notification@30e84927 OnNext 3] +3 +[rx.Notification@30e84928 OnNext 4] +``` + +Note how `takeWhile` produces 4 values and `takeUntil` produces 3. + +##### switchIfEmpty + +The new `switchIfEmpty` operator is a companion to `defaultIfEmpty` that switches to a different `Observable` if the primary `Observable` is empty. + +```java +Observable.empty() + .switchIfEmpty(Observable.just(1, 2, 3)) + .forEach(System.out::println); +``` + +#### Enhancements + +##### merge(maxConcurrent) with backpressure + +This release adds backpressure to `merge(maxConcurrent)` so that horizontal buffer bloat can also be controll with the `maxConcurrent` parameter. + +This allows parallel execution such as the following to work with backpressure: + +```java +public class MergeMaxConcurrent { + + public static void main(String[] args) { + // define 1,000,000 async tasks + Observable> asyncWork = range(1, 1000000) + .doOnNext(i -> System.out.println("Value: " + i)) + .doOnRequest(r -> System.out.println("request1 " + r)) + .map(item -> { + return just(item) + // simulate slow IO or computation + .doOnNext(MergeMaxConcurrent::sleep) + .subscribeOn(Schedulers.io()); + }) + .doOnRequest(r -> System.out.println("request2 " + r)); + + // allow 10 outstanding tasks at a time + merge(asyncWork, 10).toBlocking().forEach(System.out::println); + } + + public static void sleep(int value) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} +``` + +In prior versions all 1,000,000 tasks are immediately emitted and queued for execution. As of this release it correctly allows 10 at a time to be emitted. + +#### Changes + + +* [Pull 2493] (https://github.com/ReactiveX/RxJava/pull/2493) Experimental Operator TakeUntil with predicate +* [Pull 2585] (https://github.com/ReactiveX/RxJava/pull/2585) Experimental Operator: switchIfEmpty +* [Pull 2470] (https://github.com/ReactiveX/RxJava/pull/2470) Experimental Subject state information methods & bounded ReplaySubject termination +* [Pull 2540] (https://github.com/ReactiveX/RxJava/pull/2540) Merge with max concurrency now supports backpressure. +* [Pull 2332] (https://github.com/ReactiveX/RxJava/pull/2332) Operator retry test fix attempt +* [Pull 2244] (https://github.com/ReactiveX/RxJava/pull/2244) OperatorTakeLast add check for isUnsubscribed to fast path +* [Pull 2469] (https://github.com/ReactiveX/RxJava/pull/2469) Remove the execute permission from source files +* [Pull 2455] (https://github.com/ReactiveX/RxJava/pull/2455) Fix for #2191 - OperatorMulticast fails to unsubscribe from source +* [Pull 2474] (https://github.com/ReactiveX/RxJava/pull/2474) MergeTest.testConcurrency timeout to let other tests run +* [Pull 2335] (https://github.com/ReactiveX/RxJava/pull/2335) A set of stateless operators that don't need to be instantiated +* [Pull 2447] (https://github.com/ReactiveX/RxJava/pull/2447) Fail early if a null subscription is added to a CompositeSubscription. +* [Pull 2475] (https://github.com/ReactiveX/RxJava/pull/2475) SynchronousQueue.clone fix +* [Pull 2477] (https://github.com/ReactiveX/RxJava/pull/2477) Backpressure tests fix0121 +* [Pull 2476] (https://github.com/ReactiveX/RxJava/pull/2476) Fixed off-by-one error and value-drop in the window operator. +* [Pull 2478] (https://github.com/ReactiveX/RxJava/pull/2478) RefCountAsync: adjusted time values as 1 ms is unreliable +* [Pull 2238] (https://github.com/ReactiveX/RxJava/pull/2238) Fix the bug that cache doesn't unsubscribe the source Observable when the source is terminated +* [Pull 1840] (https://github.com/ReactiveX/RxJava/pull/1840) Unsubscribe when thread is interrupted +* [Pull 2471] (https://github.com/ReactiveX/RxJava/pull/2471) Fixes NPEs reported in ReactiveX#1702 by synchronizing queue. +* [Pull 2482] (https://github.com/ReactiveX/RxJava/pull/2482) Merge: fixed hangs & missed scalar emissions +* [Pull 2547] (https://github.com/ReactiveX/RxJava/pull/2547) Warnings cleanup +* [Pull 2465] (https://github.com/ReactiveX/RxJava/pull/2465) ScheduledExecutorService: call purge periodically on JDK 6 to avoid cancelled task-retention +* [Pull 2591] (https://github.com/ReactiveX/RxJava/pull/2591) Changed the naming of the NewThreadWorker's system parameters +* [Pull 2543] (https://github.com/ReactiveX/RxJava/pull/2543) OperatorMerge handle request overflow +* [Pull 2548] (https://github.com/ReactiveX/RxJava/pull/2548) Subscriber.request should throw exception if negative request made +* [Pull 2550] (https://github.com/ReactiveX/RxJava/pull/2550) Subscriber.onStart requests should be additive (and check for overflow) +* [Pull 2553] (https://github.com/ReactiveX/RxJava/pull/2553) RxRingBuffer with synchronization +* [Pull 2565] (https://github.com/ReactiveX/RxJava/pull/2565) Obstruction detection in tests. +* [Pull 2563] (https://github.com/ReactiveX/RxJava/pull/2563) Retry backpressure test: split error conditions into separate test lines. +* [Pull 2572] (https://github.com/ReactiveX/RxJava/pull/2572) Give more time to certain concurrency tests. +* [Pull 2559] (https://github.com/ReactiveX/RxJava/pull/2559) OnSubscribeFromIterable - add request overflow check +* [Pull 2574] (https://github.com/ReactiveX/RxJava/pull/2574) SizeEviction test needs to return false +* [Pull 2561] (https://github.com/ReactiveX/RxJava/pull/2561) Updating queue code from JCTools +* [Pull 2566] (https://github.com/ReactiveX/RxJava/pull/2566) CombineLatest: fixed concurrent requestUpTo yielding -1 requests +* [Pull 2552] (https://github.com/ReactiveX/RxJava/pull/2552) Publish: fixed incorrect subscriber requested accounting +* [Pull 2583] (https://github.com/ReactiveX/RxJava/pull/2583) Added perf tests for various container-like subscriptions +* [Pull 1955] (https://github.com/ReactiveX/RxJava/pull/1955) OnBackpressureXXX: support for common drain manager & fix for former concurrency bugs +* [Pull 2590] (https://github.com/ReactiveX/RxJava/pull/2590) Zip: fixed unbounded downstream requesting above Long.MAX_VALUE +* [Pull 2589] (https://github.com/ReactiveX/RxJava/pull/2589) Repeat/retry: fixed unbounded downstream requesting above Long.MAX_VALUE +* [Pull 2567] (https://github.com/ReactiveX/RxJava/pull/2567) RefCount: disconnect all if upstream terminates +* [Pull 2593] (https://github.com/ReactiveX/RxJava/pull/2593) Zip: emit onCompleted without waiting for request + avoid re-reading fields + + ### Version 1.0.4 – December 29th 2014 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.4%7C)) ### * [Pull 2156] (https://github.com/ReactiveX/RxJava/pull/2156) Fix the issue that map may swallow fatal exceptions From 5ee32a5d6b81a3400f2182e2287d87d288b8dcdd Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 3 Feb 2015 22:31:04 -0800 Subject: [PATCH 123/857] Gradle & Travis Release Config Configuring Travis for build and release. This was tested at https://github.com/ReactiveX/BuildInfrastructure --- .travis.yml | 7 +++++++ gradle/buildViaTravis.sh | 6 ++++++ gradle/publishViaTravis.sh | 12 ++++++++++++ 3 files changed, 25 insertions(+) create mode 100755 gradle/buildViaTravis.sh create mode 100755 gradle/publishViaTravis.sh diff --git a/.travis.yml b/.travis.yml index c228c30b5e..3cb3b13863 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,10 @@ jdk: sudo: false # as per http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ + +script: gradle/buildViaTravis.sh +after_success: +- gradle/publishViaTravis.sh + +secure: "HzRt91B6CLkBCxRXlo7V+F5L8SSHSW8dgkO8nAlTnOIK73k3QelDeBlcm1wnuNa3x+54YS9WBv6QrTNZr9lVi/8IPfKpd+jtjIbCWRvh6aNhqLIXWTL7oTvUd4E8DDUAKB6UMae6SiBSy2wsFELGHXeNwg7EiPfxsd5sKRiS7H4=" +secure: "MSZLPasqNKAC+8qhKQD3xO+ZbuOy65HpUN+1+KnoOLMkHCu/f4x60W1tpTAzn1DFEVpokHR0n3I4z4HpWybURDQfDHD1bB4IsznjCUBYA9Uo9Sb0U4TS17dQr8s7SORIjHDLGNSWETJjrA9TfuUV6HTVhRO1ECx3H+wuTwCVDN0=" diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh new file mode 100755 index 0000000000..e58e754d4d --- /dev/null +++ b/gradle/buildViaTravis.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# This script will build the project. + +echo -e 'Build Script => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + +./gradlew -Prelease.useLastTag=true build diff --git a/gradle/publishViaTravis.sh b/gradle/publishViaTravis.sh new file mode 100755 index 0000000000..2fd3432b5d --- /dev/null +++ b/gradle/publishViaTravis.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# This script will upload to Bintray. It is intended to be conditionally executed on tagged builds. + +echo -e 'Bintray Upload Script => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + +if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then + echo -e 'Bintray Upload => Starting upload ...\n' + + ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace +else + echo 'Bintray Upload => Not a tagged build so will not upload' +fi From 3938127196f8bd50b015034bac3cac04ac68d25c Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 3 Feb 2015 22:53:07 -0800 Subject: [PATCH 124/857] Nebula Config for Travis Build/Release --- build.gradle | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.gradle b/build.gradle index 4d55d340de..52fdb4fc47 100644 --- a/build.gradle +++ b/build.gradle @@ -17,3 +17,12 @@ javadoc { exclude "**/rx/internal/**" } +nebulaRelease { + addReleaseBranchPattern(/\d+\.\d+\.\d+/) + addReleaseBranchPattern('HEAD') +} + +if (project.hasProperty('release.useLastTag')) { + tasks.prepare.enabled = false +} + From 4aadb55921ec532c5f30c8b9c046820d7ec2a622 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 3 Feb 2015 23:03:31 -0800 Subject: [PATCH 125/857] Secure Variables for Release --- .travis.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3cb3b13863..b6e3d6fe89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,12 @@ jdk: sudo: false # as per http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ +# script for build and release via Travis to Bintray script: gradle/buildViaTravis.sh -after_success: -- gradle/publishViaTravis.sh +after_success: gradle/publishViaTravis.sh -secure: "HzRt91B6CLkBCxRXlo7V+F5L8SSHSW8dgkO8nAlTnOIK73k3QelDeBlcm1wnuNa3x+54YS9WBv6QrTNZr9lVi/8IPfKpd+jtjIbCWRvh6aNhqLIXWTL7oTvUd4E8DDUAKB6UMae6SiBSy2wsFELGHXeNwg7EiPfxsd5sKRiS7H4=" -secure: "MSZLPasqNKAC+8qhKQD3xO+ZbuOy65HpUN+1+KnoOLMkHCu/f4x60W1tpTAzn1DFEVpokHR0n3I4z4HpWybURDQfDHD1bB4IsznjCUBYA9Uo9Sb0U4TS17dQr8s7SORIjHDLGNSWETJjrA9TfuUV6HTVhRO1ECx3H+wuTwCVDN0=" +# secure environment variables for release to Bintray +env: + global: + - secure: "HzRt91B6CLkBCxRXlo7V+F5L8SSHSW8dgkO8nAlTnOIK73k3QelDeBlcm1wnuNa3x+54YS9WBv6QrTNZr9lVi/8IPfKpd+jtjIbCWRvh6aNhqLIXWTL7oTvUd4E8DDUAKB6UMae6SiBSy2wsFELGHXeNwg7EiPfxsd5sKRiS7H4=" + - secure: "MSZLPasqNKAC+8qhKQD3xO+ZbuOy65HpUN+1+KnoOLMkHCu/f4x60W1tpTAzn1DFEVpokHR0n3I4z4HpWybURDQfDHD1bB4IsznjCUBYA9Uo9Sb0U4TS17dQr8s7SORIjHDLGNSWETJjrA9TfuUV6HTVhRO1ECx3H+wuTwCVDN0=" From e5b7f3da684247acbfc62aaded1bdbf7816eb3dc Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Feb 2015 15:50:46 +0100 Subject: [PATCH 126/857] Added common throwIfAny to throw 0 or more exceptions --- src/main/java/rx/exceptions/Exceptions.java | 32 +++++++++++++++++-- .../internal/operators/OperatorPublish.java | 28 ++++------------ .../rx/internal/util/SubscriptionList.java | 17 ++-------- .../internal/util/SubscriptionRandomList.java | 17 ++-------- src/main/java/rx/subjects/AsyncSubject.java | 12 ++----- .../java/rx/subjects/BehaviorSubject.java | 12 ++----- src/main/java/rx/subjects/PublishSubject.java | 12 ++----- src/main/java/rx/subjects/ReplaySubject.java | 17 +++------- .../subscriptions/CompositeSubscription.java | 17 ++-------- 9 files changed, 51 insertions(+), 113 deletions(-) diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index ce778e3fa9..fda282f86d 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -15,8 +15,7 @@ */ package rx.exceptions; -import java.util.HashSet; -import java.util.Set; +import java.util.*; /** * @warn javadoc class description missing @@ -148,5 +147,32 @@ public static Throwable getFinalCause(Throwable e) { } return e; } - + /** + * Throws a single or multiple exceptions contained in the collection, wrapping it into + * {@code CompositeException} if necessary. + * @param exceptions the collection of exceptions. If null or empty, no exception is thrown. + * If the collection contains a single exception, that exception is either thrown as-is or wrapped into a + * CompositeException. Multiple exceptions are wrapped into a CompositeException. + * @param whileText the circumstance string to be appended to the thrown CompositeException, inserted after + * the sentences "Exception" and "Multiple exceptions". + */ + public static void throwIfAny(Collection exceptions, String whileText) { + if (exceptions != null && !exceptions.isEmpty()) { + if (exceptions.size() == 1) { + Throwable t = exceptions.iterator().next(); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else + if (t instanceof Error) { + throw (Error) t; + } else { + throw new CompositeException( + "Exception" + whileText, exceptions); + } + } else { + throw new CompositeException( + "Multiple exceptions" + whileText, exceptions); + } + } + } } diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 41041f9846..d7751297a9 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -15,23 +15,13 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.*; +import java.util.concurrent.atomic.*; +import rx.*; import rx.Observable; -import rx.Producer; -import rx.Subscriber; -import rx.Subscription; -import rx.exceptions.CompositeException; -import rx.exceptions.Exceptions; -import rx.exceptions.MissingBackpressureException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.exceptions.*; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observables.ConnectableObservable; import rx.subscriptions.Subscriptions; @@ -173,13 +163,7 @@ public void onError(Throwable e) { errors.add(e2); } } - if (errors != null) { - if (errors.size() == 1) { - Exceptions.propagate(errors.get(0)); - } else { - throw new CompositeException("Errors while emitting onError", errors); - } - } + Exceptions.throwIfAny(errors, " while propagating an exception."); } @Override diff --git a/src/main/java/rx/internal/util/SubscriptionList.java b/src/main/java/rx/internal/util/SubscriptionList.java index 23bddebee1..27404c92b3 100644 --- a/src/main/java/rx/internal/util/SubscriptionList.java +++ b/src/main/java/rx/internal/util/SubscriptionList.java @@ -22,7 +22,7 @@ import java.util.List; import rx.Subscription; -import rx.exceptions.CompositeException; +import rx.exceptions.*; /** * Subscription that represents a group of Subscriptions that are unsubscribed together. @@ -106,20 +106,7 @@ private static void unsubscribeFromAll(Collection subscriptions) { es.add(e); } } - if (es != null) { - if (es.size() == 1) { - Throwable t = es.get(0); - if (t instanceof RuntimeException) { - throw (RuntimeException) t; - } else { - throw new CompositeException( - "Failed to unsubscribe to 1 or more subscriptions.", es); - } - } else { - throw new CompositeException( - "Failed to unsubscribe to 2 or more subscriptions.", es); - } - } + Exceptions.throwIfAny(es, " while unsubscribing."); } /* perf support */ public void clear() { diff --git a/src/main/java/rx/internal/util/SubscriptionRandomList.java b/src/main/java/rx/internal/util/SubscriptionRandomList.java index c694508076..9c5ee5ebeb 100644 --- a/src/main/java/rx/internal/util/SubscriptionRandomList.java +++ b/src/main/java/rx/internal/util/SubscriptionRandomList.java @@ -23,7 +23,7 @@ import java.util.Set; import rx.Subscription; -import rx.exceptions.CompositeException; +import rx.exceptions.*; import rx.functions.Action1; /** @@ -155,19 +155,6 @@ private static void unsubscribeFromAll(Collection su es.add(e); } } - if (es != null) { - if (es.size() == 1) { - Throwable t = es.get(0); - if (t instanceof RuntimeException) { - throw (RuntimeException) t; - } else { - throw new CompositeException( - "Failed to unsubscribe to 1 or more subscriptions.", es); - } - } else { - throw new CompositeException( - "Failed to unsubscribe to 2 or more subscriptions.", es); - } - } + Exceptions.throwIfAny(es, " while unsubscribing."); } } diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index d3d135346c..6a810db187 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -15,12 +15,10 @@ */ package rx.subjects; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import rx.Observer; import rx.annotations.Experimental; -import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.internal.operators.NotificationLite; @@ -122,13 +120,7 @@ public void onError(final Throwable e) { } } - if (errors != null) { - if (errors.size() == 1) { - Exceptions.propagate(errors.get(0)); - } else { - throw new CompositeException("Errors while emitting AsyncSubject.onError", errors); - } - } + Exceptions.throwIfAny(errors, " while propagating an exception."); } } diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index fee30f0d99..42389956ed 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -16,12 +16,10 @@ package rx.subjects; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import rx.Observer; import rx.annotations.Experimental; -import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.internal.operators.NotificationLite; @@ -148,13 +146,7 @@ public void onError(Throwable e) { } } - if (errors != null) { - if (errors.size() == 1) { - Exceptions.propagate(errors.get(0)); - } else { - throw new CompositeException("Errors while emitting AsyncSubject.onError", errors); - } - } + Exceptions.throwIfAny(errors, " while propagating an exception."); } } diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index a4d900ddb3..ca9747f2e9 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -15,12 +15,10 @@ */ package rx.subjects; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import rx.Observer; import rx.annotations.Experimental; -import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.internal.operators.NotificationLite; @@ -106,13 +104,7 @@ public void onError(final Throwable e) { errors.add(e2); } } - if (errors != null) { - if (errors.size() == 1) { - Exceptions.propagate(errors.get(0)); - } else { - throw new CompositeException("Errors while emitting PublishSubject.onError", errors); - } - } + Exceptions.throwIfAny(errors, " while propagating an exception."); } } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index badf09a948..af4b018f04 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -16,18 +16,15 @@ package rx.subjects; import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import rx.*; import rx.Observer; -import rx.Scheduler; import rx.annotations.Experimental; -import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.functions.*; import rx.internal.operators.NotificationLite; import rx.internal.util.UtilityFunctions; import rx.schedulers.Timestamped; @@ -394,13 +391,7 @@ public void onError(final Throwable e) { } } - if (errors != null) { - if (errors.size() == 1) { - Exceptions.propagate(errors.get(0)); - } else { - throw new CompositeException("Errors while emitting ReplaySubject.onError", errors); - } - } + Exceptions.throwIfAny(errors, " while propagating an exception."); } } diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index 777b18f04d..a39e6d238c 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -23,7 +23,7 @@ import java.util.Set; import rx.Subscription; -import rx.exceptions.CompositeException; +import rx.exceptions.*; /** * Subscription that represents a group of Subscriptions that are unsubscribed together. @@ -141,19 +141,6 @@ private static void unsubscribeFromAll(Collection subscriptions) { es.add(e); } } - if (es != null) { - if (es.size() == 1) { - Throwable t = es.get(0); - if (t instanceof RuntimeException) { - throw (RuntimeException) t; - } else { - throw new CompositeException( - "Failed to unsubscribe to 1 or more subscriptions.", es); - } - } else { - throw new CompositeException( - "Failed to unsubscribe to 2 or more subscriptions.", es); - } - } + Exceptions.throwIfAny(es, " while unsubscribing."); } } From a945b16873ebbf7919a2eddaae46ecbf8212a0bc Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Feb 2015 15:56:08 +0100 Subject: [PATCH 127/857] Adjusted subject error text. --- src/main/java/rx/internal/operators/OperatorPublish.java | 2 +- src/main/java/rx/subjects/AsyncSubject.java | 2 +- src/main/java/rx/subjects/BehaviorSubject.java | 2 +- src/main/java/rx/subjects/PublishSubject.java | 2 +- src/main/java/rx/subjects/ReplaySubject.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index d7751297a9..9ac5b50f2d 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -163,7 +163,7 @@ public void onError(Throwable e) { errors.add(e2); } } - Exceptions.throwIfAny(errors, " while propagating an exception."); + Exceptions.throwIfAny(errors, " while emitting onError"); } @Override diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index 6a810db187..cf3fe32483 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -120,7 +120,7 @@ public void onError(final Throwable e) { } } - Exceptions.throwIfAny(errors, " while propagating an exception."); + Exceptions.throwIfAny(errors, " while emitting AsyncSubject.onError"); } } diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index 42389956ed..d9c7bcaef0 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -146,7 +146,7 @@ public void onError(Throwable e) { } } - Exceptions.throwIfAny(errors, " while propagating an exception."); + Exceptions.throwIfAny(errors, " while emitting BehaviorSubject.onError"); } } diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index ca9747f2e9..c427a5ebb8 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -104,7 +104,7 @@ public void onError(final Throwable e) { errors.add(e2); } } - Exceptions.throwIfAny(errors, " while propagating an exception."); + Exceptions.throwIfAny(errors, " while emitting PublishSubject.onError"); } } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index af4b018f04..5bbe91cee1 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -391,7 +391,7 @@ public void onError(final Throwable e) { } } - Exceptions.throwIfAny(errors, " while propagating an exception."); + Exceptions.throwIfAny(errors, " while emitting ReplaySubject.onError"); } } From 4b4bea248558005100bd9774e38b573bf7fcbc86 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Feb 2015 19:18:09 +0100 Subject: [PATCH 128/857] Added experimental annotation, using propagate. --- src/main/java/rx/exceptions/Exceptions.java | 36 ++++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index fda282f86d..59e7c45e5d 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -15,7 +15,11 @@ */ package rx.exceptions; -import java.util.*; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import rx.annotations.Experimental; /** * @warn javadoc class description missing @@ -26,11 +30,26 @@ private Exceptions() { } /** - * @warn javadoc missing + * Convenience method to throw a {@code RuntimeException} and {@code Error} directly + * or wrap any other exception type into a {@code RuntimeException}. + * @param t the exception to throw directly or wrapped * @return because {@code propagate} itself throws an exception or error, this is a sort of phantom return * value; {@code propagate} does not actually return anything */ public static RuntimeException propagate(Throwable t) { + return propagate(t, null); + } + /** + * Convenience method to throw a {@code RuntimeException} and {@code Error} directly + * or wrap any other exception type into a {@code RuntimeException} with an optional custom message. + * @param t the exception to throw directly or wrapped + * @param message the optional custom message to set up the RuntimeException thrown + * in case {@code t} is a checked exception. + * @return because {@code propagate} itself throws an exception or error, this is a sort of phantom return + * value; {@code propagate} does not actually return anything + */ + @Experimental + public static RuntimeException propagate(Throwable t, String message) { /* * The return type of RuntimeException is a trick for code to be like this: * @@ -44,7 +63,7 @@ public static RuntimeException propagate(Throwable t) { } else if (t instanceof Error) { throw (Error) t; } else { - throw new RuntimeException(t); + throw new RuntimeException(message, t); } } @@ -156,19 +175,12 @@ public static Throwable getFinalCause(Throwable e) { * @param whileText the circumstance string to be appended to the thrown CompositeException, inserted after * the sentences "Exception" and "Multiple exceptions". */ + @Experimental public static void throwIfAny(Collection exceptions, String whileText) { if (exceptions != null && !exceptions.isEmpty()) { if (exceptions.size() == 1) { Throwable t = exceptions.iterator().next(); - if (t instanceof RuntimeException) { - throw (RuntimeException) t; - } else - if (t instanceof Error) { - throw (Error) t; - } else { - throw new CompositeException( - "Exception" + whileText, exceptions); - } + throw propagate(t, "Exception" + whileText); } else { throw new CompositeException( "Multiple exceptions" + whileText, exceptions); From 8937c6083c6000c4d610fc4f6b5172291dbc81dd Mon Sep 17 00:00:00 2001 From: David Gross Date: Wed, 4 Feb 2015 10:49:58 -0800 Subject: [PATCH 129/857] add @since annotation to javadocs for new takeUntil variant --- src/main/java/rx/Observable.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 53d86ead3e..7acf3df8e0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7855,19 +7855,18 @@ public final Observable takeWhile(final Func1 predicate) } /** - * Returns an Observable that first emits items emitted by the source Observable, - * checks the specified condition after each item, and - * then completes if the condition is satisfied. + * Returns an Observable that emits items emitted by the source Observable, checks the specified predicate + * for each item, and then completes if the condition is satisfied. *

- * The difference between this operator and {@link #takeWhile(Func1)} is that here, the condition is evaluated after - * the item was emitted. + * The difference between this operator and {@link #takeWhile(Func1)} is that here, the condition is + * evaluated after the item is emitted. * * @param stopPredicate * a function that evaluates an item emitted by the source Observable and returns a Boolean - * @return an Observable that first emits items emitted by the source Observable, - * checks the specified condition after each item, and - * then completes if the condition is satisfied. + * @return an Observable that first emits items emitted by the source Observable, checks the specified + * condition after each item, and then completes if the condition is satisfied. * @see Observable#takeWhile(Func1) + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public final Observable takeUntil(final Func1 stopPredicate) { From a8c306b0eedf04396a96264c8473d0eb9fd3b86c Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 4 Feb 2015 10:56:14 -0800 Subject: [PATCH 130/857] Add Snapshot Support --- gradle/buildViaTravis.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index e58e754d4d..fd5a0bc14a 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -1,6 +1,10 @@ #!/bin/bash # This script will build the project. -echo -e 'Build Script => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' - -./gradlew -Prelease.useLastTag=true build +if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then + echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace +else + echo -e 'Build Pull Request => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + ./gradlew -Prelease.useLastTag=true build +fi From 62f48b21644b990d6c07c52d2be4195d446cfd91 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 4 Feb 2015 11:07:00 -0800 Subject: [PATCH 131/857] Update buildViaTravis.sh --- gradle/buildViaTravis.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index fd5a0bc14a..ff2772eea5 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -1,10 +1,13 @@ #!/bin/bash # This script will build the project. -if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then +if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then + echo -e 'Build Pull Request => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + ./gradlew -Prelease.useLastTag=true build +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' - ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace + ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace else - echo -e 'Build Pull Request => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.useLastTag=true build fi From b3640d8949b4660aa6fa83c988447f250e0a6921 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 4 Feb 2015 11:26:43 -0800 Subject: [PATCH 132/857] Add Release Branch --- gradle/buildViaTravis.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index ff2772eea5..4b41369ff6 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -2,12 +2,14 @@ # This script will build the project. if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then - echo -e 'Build Pull Request => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + echo -e 'Build Pull Request => Branch ['$TRAVIS_BRANCH']' ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then - echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace -else +elif if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' - ./gradlew -Prelease.useLastTag=true build + ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace +else + echo -e 'ERROR: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' fi From d1bf22b18defcfef012e6494cd6b46dd364eb07a Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 4 Feb 2015 11:27:07 -0800 Subject: [PATCH 133/857] Use buildViaTravis for all flows --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b6e3d6fe89..8704714547 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ sudo: false # script for build and release via Travis to Bintray script: gradle/buildViaTravis.sh -after_success: gradle/publishViaTravis.sh # secure environment variables for release to Bintray env: From 0d4b610e20a65ce9466832db1422f9190fba75c6 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Feb 2015 22:49:08 +0100 Subject: [PATCH 134/857] Reporting stackoverflow caused stackoverflow --- src/main/java/rx/exceptions/Exceptions.java | 39 +++++++------------ .../internal/operators/OperatorPublish.java | 2 +- .../rx/internal/util/SubscriptionList.java | 2 +- .../internal/util/SubscriptionRandomList.java | 2 +- src/main/java/rx/subjects/AsyncSubject.java | 2 +- .../java/rx/subjects/BehaviorSubject.java | 2 +- src/main/java/rx/subjects/PublishSubject.java | 2 +- src/main/java/rx/subjects/ReplaySubject.java | 2 +- .../subscriptions/CompositeSubscription.java | 2 +- 9 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index 59e7c45e5d..8ac7091def 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -15,8 +15,8 @@ */ package rx.exceptions; -import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import rx.annotations.Experimental; @@ -37,19 +37,6 @@ private Exceptions() { * value; {@code propagate} does not actually return anything */ public static RuntimeException propagate(Throwable t) { - return propagate(t, null); - } - /** - * Convenience method to throw a {@code RuntimeException} and {@code Error} directly - * or wrap any other exception type into a {@code RuntimeException} with an optional custom message. - * @param t the exception to throw directly or wrapped - * @param message the optional custom message to set up the RuntimeException thrown - * in case {@code t} is a checked exception. - * @return because {@code propagate} itself throws an exception or error, this is a sort of phantom return - * value; {@code propagate} does not actually return anything - */ - @Experimental - public static RuntimeException propagate(Throwable t, String message) { /* * The return type of RuntimeException is a trick for code to be like this: * @@ -63,10 +50,9 @@ public static RuntimeException propagate(Throwable t, String message) { } else if (t instanceof Error) { throw (Error) t; } else { - throw new RuntimeException(message, t); + throw new RuntimeException(t); } } - /** * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. These * varieties are as follows: @@ -172,19 +158,24 @@ public static Throwable getFinalCause(Throwable e) { * @param exceptions the collection of exceptions. If null or empty, no exception is thrown. * If the collection contains a single exception, that exception is either thrown as-is or wrapped into a * CompositeException. Multiple exceptions are wrapped into a CompositeException. - * @param whileText the circumstance string to be appended to the thrown CompositeException, inserted after - * the sentences "Exception" and "Multiple exceptions". */ @Experimental - public static void throwIfAny(Collection exceptions, String whileText) { + public static void throwIfAny(List exceptions) { if (exceptions != null && !exceptions.isEmpty()) { if (exceptions.size() == 1) { - Throwable t = exceptions.iterator().next(); - throw propagate(t, "Exception" + whileText); - } else { - throw new CompositeException( - "Multiple exceptions" + whileText, exceptions); + Throwable t = exceptions.get(0); + // had to manually inline propagate because some tests attempt StackOverflowError + // and can't handle it with the stack space remaining + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else if (t instanceof Error) { + throw (Error) t; + } else { + throw new RuntimeException(t); + } } + throw new CompositeException( + "Multiple exceptions", exceptions); } } } diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 9ac5b50f2d..de81e93241 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -163,7 +163,7 @@ public void onError(Throwable e) { errors.add(e2); } } - Exceptions.throwIfAny(errors, " while emitting onError"); + Exceptions.throwIfAny(errors); } @Override diff --git a/src/main/java/rx/internal/util/SubscriptionList.java b/src/main/java/rx/internal/util/SubscriptionList.java index 27404c92b3..02aecdef71 100644 --- a/src/main/java/rx/internal/util/SubscriptionList.java +++ b/src/main/java/rx/internal/util/SubscriptionList.java @@ -106,7 +106,7 @@ private static void unsubscribeFromAll(Collection subscriptions) { es.add(e); } } - Exceptions.throwIfAny(es, " while unsubscribing."); + Exceptions.throwIfAny(es); } /* perf support */ public void clear() { diff --git a/src/main/java/rx/internal/util/SubscriptionRandomList.java b/src/main/java/rx/internal/util/SubscriptionRandomList.java index 9c5ee5ebeb..bc316f6abb 100644 --- a/src/main/java/rx/internal/util/SubscriptionRandomList.java +++ b/src/main/java/rx/internal/util/SubscriptionRandomList.java @@ -155,6 +155,6 @@ private static void unsubscribeFromAll(Collection su es.add(e); } } - Exceptions.throwIfAny(es, " while unsubscribing."); + Exceptions.throwIfAny(es); } } diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index cf3fe32483..4861c8b91f 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -120,7 +120,7 @@ public void onError(final Throwable e) { } } - Exceptions.throwIfAny(errors, " while emitting AsyncSubject.onError"); + Exceptions.throwIfAny(errors); } } diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index d9c7bcaef0..29936431c1 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -146,7 +146,7 @@ public void onError(Throwable e) { } } - Exceptions.throwIfAny(errors, " while emitting BehaviorSubject.onError"); + Exceptions.throwIfAny(errors); } } diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index c427a5ebb8..0a3292ae50 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -104,7 +104,7 @@ public void onError(final Throwable e) { errors.add(e2); } } - Exceptions.throwIfAny(errors, " while emitting PublishSubject.onError"); + Exceptions.throwIfAny(errors); } } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index 5bbe91cee1..5d6292be19 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -391,7 +391,7 @@ public void onError(final Throwable e) { } } - Exceptions.throwIfAny(errors, " while emitting ReplaySubject.onError"); + Exceptions.throwIfAny(errors); } } diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index a39e6d238c..3c8124f3ec 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -141,6 +141,6 @@ private static void unsubscribeFromAll(Collection subscriptions) { es.add(e); } } - Exceptions.throwIfAny(es, " while unsubscribing."); + Exceptions.throwIfAny(es); } } From 3e5d8c8e0ae28e00e7b928bac2b1d64e9f652feb Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 4 Feb 2015 14:38:10 -0800 Subject: [PATCH 135/857] Snapshot with Patch Version See https://github.com/ReactiveX/RxJava/issues/2586#issuecomment-72937790 --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 52fdb4fc47..c476256681 100644 --- a/build.gradle +++ b/build.gradle @@ -17,11 +17,15 @@ javadoc { exclude "**/rx/internal/**" } +# support for snapshot/final releases with the various branches RxJava uses nebulaRelease { addReleaseBranchPattern(/\d+\.\d+\.\d+/) addReleaseBranchPattern('HEAD') } +# the snapshot versioning should increment the path number +release.scope=patch + if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false } From 96c37422964828e983052a96bab2c4d607df7686 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 5 Feb 2015 17:12:01 +1100 Subject: [PATCH 136/857] improve OnSubscribeRefCount comments --- .../java/rx/internal/operators/OnSubscribeRefCount.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRefCount.java b/src/main/java/rx/internal/operators/OnSubscribeRefCount.java index 22cbc9062d..cc422453f2 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRefCount.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRefCount.java @@ -71,10 +71,10 @@ public void call(final Subscriber subscriber) { source.connect(onSubscribe(subscriber, writeLocked)); } finally { // need to cover the case where the source is subscribed to - // outside of this class thus preventing the above Action1 - // being called + // outside of this class thus preventing the Action1 passed + // to source.connect above being called if (writeLocked.get()) { - // Action1 was not called + // Action1 passed to source.connect was not called lock.unlock(); } } @@ -129,6 +129,8 @@ public void onCompleted() { subscriber.onCompleted(); } void cleanup() { + // on error or completion we need to unsubscribe the base subscription + // and set the subscriptionCount to 0 lock.lock(); try { if (baseSubscription == currentBase) { From 2f096dfb1ad6919898ea27492fa6eddd24cafa5b Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 5 Feb 2015 13:02:42 +0100 Subject: [PATCH 137/857] Fixed comment and missing value --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c476256681..845062cb1a 100644 --- a/build.gradle +++ b/build.gradle @@ -17,14 +17,14 @@ javadoc { exclude "**/rx/internal/**" } -# support for snapshot/final releases with the various branches RxJava uses +// support for snapshot/final releases with the various branches RxJava uses nebulaRelease { addReleaseBranchPattern(/\d+\.\d+\.\d+/) addReleaseBranchPattern('HEAD') } -# the snapshot versioning should increment the path number -release.scope=patch +// the snapshot versioning should increment the path number +// release.scope=patch if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false From 4b3fb7964374c6e20ad6055a802f738ad99eece1 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 5 Feb 2015 13:07:32 +0100 Subject: [PATCH 138/857] Script fix --- gradle/buildViaTravis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 4b41369ff6..9462f8c7e6 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -7,7 +7,7 @@ if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace -elif if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace else From 79eda4abc739b415fb8282c5e3007c6766a17dfb Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 5 Feb 2015 13:35:20 +0100 Subject: [PATCH 139/857] Build at least as if it was a pull request. --- gradle/buildViaTravis.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 9462f8c7e6..831c7349bf 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -11,5 +11,6 @@ elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace else - echo -e 'ERROR: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + ./gradlew -Prelease.useLastTag=true build fi From 8982110da2c5da93a66ba9ce807c3e3683c3f08e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 5 Feb 2015 18:05:27 +0100 Subject: [PATCH 140/857] Changed empty into a stateless constant observable. --- src/main/java/rx/Observable.java | 17 ++++++++++++----- src/test/java/rx/ObservableTests.java | 13 +++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7acf3df8e0..f1c2f68284 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -15,13 +15,11 @@ import java.util.*; import java.util.concurrent.*; -import rx.annotations.Beta; -import rx.annotations.Experimental; +import rx.annotations.*; import rx.exceptions.*; import rx.functions.*; import rx.internal.operators.*; -import rx.internal.util.ScalarSynchronousObservable; -import rx.internal.util.UtilityFunctions; +import rx.internal.util.*; import rx.observables.*; import rx.observers.SafeSubscriber; import rx.plugins.*; @@ -1031,6 +1029,14 @@ public final static Observable defer(Func0> observableFacto return create(new OnSubscribeDefer(observableFactory)); } + /** An empty observable which just emits onCompleted to any subscriber. */ + private static final Observable EMPTY = create(new OnSubscribe() { + @Override + public void call(Subscriber t1) { + t1.onCompleted(); + } + }); + /** * Returns an Observable that emits no items to the {@link Observer} and immediately invokes its * {@link Observer#onCompleted onCompleted} method. @@ -1047,8 +1053,9 @@ public final static Observable defer(Func0> observableFacto * {@link Observer}'s {@link Observer#onCompleted() onCompleted} method * @see ReactiveX operators documentation: Empty */ + @SuppressWarnings("unchecked") public final static Observable empty() { - return from(Collections.emptyList()); + return (Observable)EMPTY; } /** diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index 41aa61645b..4c7f02ca06 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -1116,4 +1116,17 @@ public void testErrorThrownIssue1685() { System.out.println("Done"); } + @Test + public void testEmptyIdentity() { + assertEquals(Observable.empty(), Observable.empty()); + } + + @Test + public void testEmptyIsEmpty() { + Observable.empty().subscribe(w); + + verify(w).onCompleted(); + verify(w, never()).onNext(any(Integer.class)); + verify(w, never()).onError(any(Throwable.class)); + } } From 3fd23c6cb6f282cd03a03f42c4d5e436fc18d11d Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 5 Feb 2015 15:11:16 -0800 Subject: [PATCH 141/857] add diagram for new takeUntil variant and a few other related javadoc tweaks and remove a troublesome unicode ligature from SpscArrayQueue.java that's causing a compiler warning --- src/main/java/rx/Observable.java | 5 +++++ src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7acf3df8e0..d550a16a1b 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7849,6 +7849,7 @@ public final Observable takeUntil(Observable other) { * @return an Observable that emits the items from the source Observable so long as each item satisfies the * condition defined by {@code predicate}, then completes * @see ReactiveX operators documentation: TakeWhile + * @see Observable#takeUntil(Func1) */ public final Observable takeWhile(final Func1 predicate) { return lift(new OperatorTakeWhile(predicate)); @@ -7858,13 +7859,17 @@ public final Observable takeWhile(final Func1 predicate) * Returns an Observable that emits items emitted by the source Observable, checks the specified predicate * for each item, and then completes if the condition is satisfied. *

+ * + *

* The difference between this operator and {@link #takeWhile(Func1)} is that here, the condition is * evaluated after the item is emitted. * + * @warn "Scheduler" and "Backpressure Support" sections missing from javadocs * @param stopPredicate * a function that evaluates an item emitted by the source Observable and returns a Boolean * @return an Observable that first emits items emitted by the source Observable, checks the specified * condition after each item, and then completes if the condition is satisfied. + * @see ReactiveX operators documentation: TakeUntil * @see Observable#takeWhile(Func1) * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ diff --git a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java index 6064106503..16d40b6951 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java @@ -96,7 +96,7 @@ public SpscArrayQueueL3Pad(int capacity) { * Flow), and adjusted to comply with Queue.offer semantics with regards to capacity.
* For convenience the relevant papers are available in the resources folder:
* 2010 - Pisa - SPSC Queues on Shared Cache Multi-Core Systems.pdf
- * 2012 - Junchang- BQueue- Efficient and Practical Queuing.pdf
+ * 2012 - Junchang- BQueue- Efficient and Practical Queuing.pdf
*
This implementation is wait free. * * @author nitsanw From cdc0ae3b0c33afe30f2dd50f1e7a6b64cd444f30 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 5 Feb 2015 22:17:21 -0800 Subject: [PATCH 142/857] Add release.travisci Property As per https://github.com/ReactiveX/RxJava/issues/2586#issuecomment-73136205 --- gradle/buildViaTravis.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 831c7349bf..3484106e31 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -6,10 +6,10 @@ if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - ./gradlew -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' - ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace + ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace else echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.useLastTag=true build From e7fe69df271726786d53061f917a92fb2655f681 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 5 Feb 2015 22:24:41 -0800 Subject: [PATCH 143/857] Disable Snapshot --- gradle/buildViaTravis.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 3484106e31..088a7e6f72 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -6,7 +6,8 @@ if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace + #./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace From 0dadcdecaf4acc636835ae77b625a336aa5eb6b3 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 6 Feb 2015 09:42:00 +0100 Subject: [PATCH 144/857] FlatMap overloads with maximum concurrency parameter --- src/main/java/rx/Observable.java | 94 ++++++++++++- .../operators/OperatorFlatMapTest.java | 124 ++++++++++++++++-- 2 files changed, 207 insertions(+), 11 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index d550a16a1b..f68aaf1915 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4509,7 +4509,34 @@ public final Observable firstOrDefault(T defaultValue, Func1 Observable flatMap(Func1> func) { return merge(map(func)); } - + + /** + * Returns an Observable that emits items based on applying a function that you supply to each item emitted + * by the source Observable, where that function returns an Observable, and then merging those resulting + * Observables and emitting the results of this merger, while limiting the maximum number of concurrent + * subscriptions to these Observables. + *

+ * + *

+ *
Scheduler:
+ *
{@code flatMap} 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 + * @param maxConcurrent + * the maximum number of Observables that may be subscribed to concurrently + * @return an Observable that emits the result of applying the transformation function to each item emitted + * by the source Observable and merging the results of the Observables obtained from this + * transformation + * @see ReactiveX operators documentation: FlatMap + */ + @Beta + public final Observable flatMap(Func1> func, int maxConcurrent) { + return merge(map(func), maxConcurrent); + } + /** * Returns an Observable that applies a function to each item emitted or notification raised by the source * Observable and then flattens the Observables returned from these functions and emits the resulting items. @@ -4540,6 +4567,40 @@ public final Observable flatMap( Func0> onCompleted) { return merge(mapNotification(onNext, onError, onCompleted)); } + /** + * Returns an Observable that applies a function to each item emitted or notification raised by the source + * Observable and then flattens the Observables returned from these functions and emits the resulting items, + * while limiting the maximum number of concurrent subscriptions to these Observables. + *

+ * + *

+ *
Scheduler:
+ *
{@code flatMap} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the result type + * @param onNext + * a function that returns an Observable to merge for each item emitted by the source Observable + * @param onError + * a function that returns an Observable to merge for an onError notification from the source + * Observable + * @param onCompleted + * a function that returns an Observable to merge for an onCompleted notification from 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 Observables returned from applying the + * specified functions to the emissions and notifications of the source Observable + * @see ReactiveX operators documentation: FlatMap + */ + @Beta + public final Observable flatMap( + Func1> onNext, + Func1> onError, + Func0> onCompleted, int maxConcurrent) { + return merge(mapNotification(onNext, onError, onCompleted), maxConcurrent); + } /** * Returns an Observable that emits the results of a specified function to the pair of values emitted by the @@ -4568,6 +4629,37 @@ public final Observable flatMap(final Func1 resultSelector) { return merge(lift(new OperatorMapPair(collectionSelector, resultSelector))); } + /** + * Returns an Observable that emits the results of a specified function to the pair of values emitted by the + * source Observable and a specified collection Observable, while limiting the maximum number of concurrent + * subscriptions to these Observables. + *

+ * + *

+ *
Scheduler:
+ *
{@code flatMap} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of items emitted by the collection Observable + * @param + * the type of items emitted by the resulting Observable + * @param collectionSelector + * a function that returns an Observable for each item emitted by the source Observable + * @param resultSelector + * a function that combines one item emitted by each of the source and collection Observables and + * returns an item to be emitted by the resulting Observable + * @param maxConcurrent + * the maximum number of Observables that may be subscribed to concurrently + * @return an Observable that emits the results of applying a function to a pair of values emitted by the + * source Observable and the collection Observable + * @see ReactiveX operators documentation: FlatMap + */ + @Beta + public final Observable flatMap(final Func1> collectionSelector, + final Func2 resultSelector, int maxConcurrent) { + return merge(lift(new OperatorMapPair(collectionSelector, resultSelector)), maxConcurrent); + } /** * Returns an Observable that merges each item emitted by the source Observable with the values in an diff --git a/src/test/java/rx/internal/operators/OperatorFlatMapTest.java b/src/test/java/rx/internal/operators/OperatorFlatMapTest.java index 3a696a0c93..a4635f1512 100644 --- a/src/test/java/rx/internal/operators/OperatorFlatMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorFlatMapTest.java @@ -16,22 +16,20 @@ package rx.internal.operators; import static org.mockito.Matchers.any; -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 static org.mockito.Mockito.*; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; +import org.junit.*; import rx.Observable; import rx.Observer; import rx.exceptions.TestException; -import rx.functions.Func0; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.functions.*; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; public class OperatorFlatMapTest { @Test @@ -312,4 +310,110 @@ public void testFlatMapTransformsMergeException() { verify(o, never()).onNext(any()); verify(o, never()).onCompleted(); } + + private static Observable compose(Observable source, final AtomicInteger subscriptionCount, final int m) { + return source.doOnSubscribe(new Action0() { + @Override + public void call() { + if (subscriptionCount.getAndIncrement() >= m) { + Assert.fail("Too many subscriptions! " + subscriptionCount.get()); + } + } + }).doOnCompleted(new Action0() { + @Override + public void call() { + if (subscriptionCount.decrementAndGet() < 0) { + Assert.fail("Too many unsubscriptionss! " + subscriptionCount.get()); + } + } + }); + } + + @Test + public void testFlatMapMaxConcurrent() { + final int m = 4; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Observable source = Observable.range(1, 10).flatMap(new Func1>() { + @Override + public Observable call(Integer t1) { + return compose(Observable.range(t1 * 10, 2), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + } + }, m); + + TestSubscriber ts = new TestSubscriber(); + + source.subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + Set expected = new HashSet(Arrays.asList( + 10, 11, 20, 21, 30, 31, 40, 41, 50, 51, 60, 61, 70, 71, 80, 81, 90, 91, 100, 101 + )); + Assert.assertEquals(expected.size(), ts.getOnNextEvents().size()); + Assert.assertTrue(expected.containsAll(ts.getOnNextEvents())); + } + @Test + public void testFlatMapSelectorMaxConcurrent() { + final int m = 4; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Observable source = Observable.range(1, 10).flatMap(new Func1>() { + @Override + public Observable call(Integer t1) { + return compose(Observable.range(t1 * 10, 2), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + } + }, new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return t1 * 1000 + t2; + } + }, m); + + TestSubscriber ts = new TestSubscriber(); + + source.subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + Set expected = new HashSet(Arrays.asList( + 1010, 1011, 2020, 2021, 3030, 3031, 4040, 4041, 5050, 5051, + 6060, 6061, 7070, 7071, 8080, 8081, 9090, 9091, 10100, 10101 + )); + Assert.assertEquals(expected.size(), ts.getOnNextEvents().size()); + System.out.println("--> testFlatMapSelectorMaxConcurrent: " + ts.getOnNextEvents()); + Assert.assertTrue(expected.containsAll(ts.getOnNextEvents())); + } + @Test + public void testFlatMapTransformsMaxConcurrentNormal() { + final int m = 2; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Observable onNext = + compose(Observable.from(Arrays.asList(1, 2, 3)).observeOn(Schedulers.computation()), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + Observable onCompleted = compose(Observable.from(Arrays.asList(4)), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + Observable onError = Observable.from(Arrays.asList(5)); + + Observable source = Observable.from(Arrays.asList(10, 20, 30)); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + TestSubscriber ts = new TestSubscriber(o); + + source.flatMap(just(onNext), just(onError), just0(onCompleted), m).subscribe(ts); + + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + verify(o, times(3)).onNext(1); + verify(o, times(3)).onNext(2); + verify(o, times(3)).onNext(3); + verify(o).onNext(4); + verify(o).onCompleted(); + + verify(o, never()).onNext(5); + verify(o, never()).onError(any(Throwable.class)); + } } From c33c0d94ae909cac53001514bf161293fb8f9384 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 6 Feb 2015 09:45:22 +0100 Subject: [PATCH 145/857] Make travis run a proper build. --- gradle/buildViaTravis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 088a7e6f72..9cd9d5f5f6 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -7,7 +7,7 @@ if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' #./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" --stacktrace + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" build --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace From 2f58536a9ffce892f013db9671de7cdb611e9a74 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 6 Feb 2015 12:44:56 +0100 Subject: [PATCH 146/857] Cast back Observer to Subscriber if passed to subscribe(Observer) --- src/main/java/rx/Observable.java | 3 +++ src/test/java/rx/ObservableTests.java | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 01fcf4ab0b..82f3532a6c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7289,6 +7289,9 @@ public final void onNext(T args) { * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(final Observer observer) { + if (observer instanceof Subscriber) { + return subscribe((Subscriber)observer); + } return subscribe(new Subscriber() { @Override diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index 4c7f02ca06..e5ed491da1 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -1129,4 +1129,13 @@ public void testEmptyIsEmpty() { verify(w, never()).onNext(any(Integer.class)); verify(w, never()).onError(any(Throwable.class)); } + + @Test // cf. https://github.com/ReactiveX/RxJava/issues/2599 + public void testSubscribingSubscriberAsObserverMaintainsSubscriptionChain() { + TestSubscriber subscriber = new TestSubscriber(); + Subscription subscription = Observable.just("event").subscribe((Observer) subscriber); + subscription.unsubscribe(); + + subscriber.assertUnsubscribed(); + } } From 67cc31bc5807ab58e2634aad3d7e87540ad874a9 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 6 Feb 2015 12:52:55 -0800 Subject: [PATCH 147/857] Reenable Snapshot --- gradle/buildViaTravis.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 9cd9d5f5f6..3484106e31 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -6,8 +6,7 @@ if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - #./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" build --stacktrace + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace From b205ec50b9d8b028c289e80301d750fa609a66a3 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 6 Feb 2015 13:17:35 -0800 Subject: [PATCH 148/857] release.scope=patch --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 845062cb1a..fcd424bcaa 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ nebulaRelease { } // the snapshot versioning should increment the path number -// release.scope=patch +release.scope=patch if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false From ce8cfdf88d465bb97db956be341ef8a5a616517a Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 6 Feb 2015 13:21:35 -0800 Subject: [PATCH 149/857] reverting release.scope=patch --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fcd424bcaa..be9eed36c0 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ nebulaRelease { } // the snapshot versioning should increment the path number -release.scope=patch +// release.scope=patch // this isn't working yet if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false From 55fa484236bd1b5c04a6c64d4e53907048596841 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 6 Feb 2015 13:57:37 -0800 Subject: [PATCH 150/857] project.setProperty('release.scope', 'patch') as per https://github.com/ReactiveX/RxJava/issues/2586#issuecomment-73316386 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index be9eed36c0..cafcf7a929 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ nebulaRelease { } // the snapshot versioning should increment the path number -// release.scope=patch // this isn't working yet +project.setProperty('release.scope', 'patch') if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false From 4da230dc131d86b7d51c281e04d249e7c7ffc2f0 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 6 Feb 2015 14:52:19 -0800 Subject: [PATCH 151/857] Update build.gradle I don't enjoy trial and error with build systems. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cafcf7a929..e5478e54e5 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ nebulaRelease { } // the snapshot versioning should increment the path number -project.setProperty('release.scope', 'patch') +// project.setProperty('release.scope', 'patch') if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false From e916b7e0593b48d303854650cee89f0e0a7a5e3a Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 6 Feb 2015 15:47:40 -0800 Subject: [PATCH 152/857] Create gradle.properties as per https://github.com/ReactiveX/RxJava/issues/2586#issuecomment-73331618 --- gradle.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 gradle.properties diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000..ef60329843 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +release.scope=patch From 68f7f668eafe3ebc1987fea09f66954975e9bdc4 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 6 Feb 2015 15:48:04 -0800 Subject: [PATCH 153/857] Update build.gradle --- build.gradle | 3 --- 1 file changed, 3 deletions(-) diff --git a/build.gradle b/build.gradle index e5478e54e5..d07776fb1d 100644 --- a/build.gradle +++ b/build.gradle @@ -23,9 +23,6 @@ nebulaRelease { addReleaseBranchPattern('HEAD') } -// the snapshot versioning should increment the path number -// project.setProperty('release.scope', 'patch') - if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false } From faf9d65f61ed0958e896c600198836ee1eb856f4 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 9 Feb 2015 20:15:12 +0800 Subject: [PATCH 154/857] Fix a potential memory leak in schedulePeriodically --- src/main/java/rx/Scheduler.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Scheduler.java b/src/main/java/rx/Scheduler.java index eac61cb120..03359a3f38 100644 --- a/src/main/java/rx/Scheduler.java +++ b/src/main/java/rx/Scheduler.java @@ -20,6 +20,7 @@ import rx.functions.Action0; import rx.schedulers.Schedulers; import rx.subscriptions.MultipleAssignmentSubscription; +import rx.subscriptions.SerialSubscription; /** * A {@code Scheduler} is an object that schedules units of work. You can find common implementations of this @@ -119,11 +120,17 @@ public void call() { if (!mas.isUnsubscribed()) { action.call(); long nextTick = startInNanos + (++count * periodInNanos); - mas.set(schedule(this, nextTick - TimeUnit.MILLISECONDS.toNanos(now()), TimeUnit.NANOSECONDS)); + SerialSubscription s = new SerialSubscription(); + // Should call `mas.set` before `schedule`, or the new Subscription may replace the old one. + mas.set(s); + s.set(schedule(this, nextTick - TimeUnit.MILLISECONDS.toNanos(now()), TimeUnit.NANOSECONDS)); } } }; - mas.set(schedule(recursiveAction, initialDelay, unit)); + SerialSubscription s = new SerialSubscription(); + // Should call `mas.set` before `schedule`, or the new Subscription may replace the old one. + mas.set(s); + s.set(schedule(recursiveAction, initialDelay, unit)); return mas; } From c694a2adaafa52514e88cfdf6379516c9494379a Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 9 Feb 2015 20:51:14 +0800 Subject: [PATCH 155/857] Revert the change in recursiveAction --- src/main/java/rx/Scheduler.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/rx/Scheduler.java b/src/main/java/rx/Scheduler.java index 03359a3f38..9f9fa91a3c 100644 --- a/src/main/java/rx/Scheduler.java +++ b/src/main/java/rx/Scheduler.java @@ -120,10 +120,7 @@ public void call() { if (!mas.isUnsubscribed()) { action.call(); long nextTick = startInNanos + (++count * periodInNanos); - SerialSubscription s = new SerialSubscription(); - // Should call `mas.set` before `schedule`, or the new Subscription may replace the old one. - mas.set(s); - s.set(schedule(this, nextTick - TimeUnit.MILLISECONDS.toNanos(now()), TimeUnit.NANOSECONDS)); + mas.set(schedule(this, nextTick - TimeUnit.MILLISECONDS.toNanos(now()), TimeUnit.NANOSECONDS)); } } }; From 9f590cf43f370cb40dc57698e6408a3c5d5a52ea Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 9 Feb 2015 22:29:48 +0800 Subject: [PATCH 156/857] Use MultipleAssignmentSubscription to save an import --- src/main/java/rx/Scheduler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/rx/Scheduler.java b/src/main/java/rx/Scheduler.java index 9f9fa91a3c..12922bc4a4 100644 --- a/src/main/java/rx/Scheduler.java +++ b/src/main/java/rx/Scheduler.java @@ -20,7 +20,6 @@ import rx.functions.Action0; import rx.schedulers.Schedulers; import rx.subscriptions.MultipleAssignmentSubscription; -import rx.subscriptions.SerialSubscription; /** * A {@code Scheduler} is an object that schedules units of work. You can find common implementations of this @@ -124,7 +123,7 @@ public void call() { } } }; - SerialSubscription s = new SerialSubscription(); + MultipleAssignmentSubscription s = new MultipleAssignmentSubscription(); // Should call `mas.set` before `schedule`, or the new Subscription may replace the old one. mas.set(s); s.set(schedule(recursiveAction, initialDelay, unit)); From 494edb4ff48c00f808e6f53b2dac103982ca02ae Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Tue, 10 Feb 2015 16:23:24 +0100 Subject: [PATCH 157/857] Missing Unsafe class yields NoClassDefFoundError so checking for Exception is not really appropriate. Let's catch all throwables. --- src/main/java/rx/internal/util/unsafe/UnsafeAccess.java | 2 +- 1 file changed, 1 insertion(+), 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 3aef5242ac..a5cbcb5d40 100644 --- a/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java +++ b/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java @@ -41,7 +41,7 @@ private UnsafeAccess() { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); u = (Unsafe) field.get(null); - } catch (Exception e) { + } catch (Throwable e) { // do nothing, hasUnsafe() will return false } UNSAFE = u; From a128c5eea38ab5098b571cccda9d3ce5095a84c6 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 11 Feb 2015 09:10:07 +0100 Subject: [PATCH 158/857] TakeWhile: don't unsubscribe downstream. --- src/main/java/rx/Subscriber.java | 17 ++++- .../internal/operators/OperatorTakeWhile.java | 4 +- .../operators/OperatorTakeWhileTest.java | 63 +++++++++++++++---- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 5522e3c326..4f87002f2c 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -40,13 +40,24 @@ public abstract class Subscriber implements Observer, Subscription { private long requested = Long.MIN_VALUE; // default to not set protected Subscriber() { - this.op = null; - this.cs = new SubscriptionList(); + this(null, false); } protected Subscriber(Subscriber op) { + this(op, true); + } + /** + * Construct a subscriber by using the other subscriber for backpressure + * and optionally sharing the underlying subscriptions list. + *

To retain the chaining of subscribers, the caller should add the + * created instance to the op via {@code add()}. + * + * @param op the other subscriber + * @param shareSubscriptions should the subscription list in op shared with this instance? + */ + protected Subscriber(Subscriber op, boolean shareSubscriptions) { this.op = op; - this.cs = op.cs; + this.cs = shareSubscriptions && op != null ? op.cs : new SubscriptionList(); } /** diff --git a/src/main/java/rx/internal/operators/OperatorTakeWhile.java b/src/main/java/rx/internal/operators/OperatorTakeWhile.java index 2f59e7b032..121ac0cc08 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeWhile.java +++ b/src/main/java/rx/internal/operators/OperatorTakeWhile.java @@ -45,7 +45,7 @@ public OperatorTakeWhile(Func2 predicate) { @Override public Subscriber call(final Subscriber subscriber) { - return new Subscriber(subscriber) { + Subscriber s = new Subscriber(subscriber, false) { private int counter = 0; @@ -86,6 +86,8 @@ public void onError(Throwable e) { } }; + subscriber.add(s); + return s; } } diff --git a/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java b/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java index eca54f7a29..33e5b2c881 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java @@ -17,22 +17,17 @@ import static org.junit.Assert.fail; import static org.mockito.Matchers.any; -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 static org.mockito.Mockito.*; -import org.junit.Test; +import java.util.Arrays; -import rx.Observable; +import org.junit.*; + +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Subscriber; -import rx.Subscription; import rx.functions.Func1; -import rx.functions.Func2; -import rx.subjects.PublishSubject; -import rx.subjects.Subject; +import rx.observers.TestSubscriber; +import rx.subjects.*; public class OperatorTakeWhileTest { @@ -222,4 +217,48 @@ public void run() { System.out.println("done starting TestObservable thread"); } } + + @Test + public void testBackpressure() { + Observable source = Observable.range(1, 1000).takeWhile(new Func1() { + @Override + public Boolean call(Integer t1) { + return t1 < 100; + } + }); + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(5); + } + }; + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); + + ts.requestMore(5); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + } + + @Test + public void testNoUnsubscribeDownstream() { + Observable source = Observable.range(1, 1000).takeWhile(new Func1() { + @Override + public Boolean call(Integer t1) { + return t1 < 2; + } + }); + TestSubscriber ts = new TestSubscriber(); + + source.unsafeSubscribe(ts); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1)); + + Assert.assertFalse("Unsubscribed!", ts.isUnsubscribed()); + } } From bb5fd6b453e4632d36ebc8f07b9c7dfe5eba7965 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 11 Feb 2015 14:03:14 -0800 Subject: [PATCH 159/857] Fix Pull Request Conditionals --- gradle/buildViaTravis.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 3484106e31..aa8bb98824 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -1,16 +1,16 @@ #!/bin/bash # This script will build the project. -if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then - echo -e 'Build Pull Request => Branch ['$TRAVIS_BRANCH']' +if [ "$TRAVIS_PULL_REQUEST" ]; then + echo -e 'Build Pull Request ['$TRAVIS_PULL_REQUEST'] => Branch ['$TRAVIS_BRANCH']' ./gradlew -Prelease.useLastTag=true build -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then +elif [ ! "$TRAVIS_PULL_REQUEST" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then +elif [ ! "$TRAVIS_PULL_REQUEST" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace else - echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' + echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' ./gradlew -Prelease.useLastTag=true build fi From 5e0dbedae4c4b2a18001316ec4a96a3b7f8066c2 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 11 Feb 2015 14:39:45 -0800 Subject: [PATCH 160/857] Version 1.0.6 --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 57d719efdb..a7e2302728 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,52 @@ # RxJava Releases # +### Version 1.0.6 – Feburary 11th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.6%7C)) ### + +This release adds an experimental operator and fixes several bugs. + +##### flatMap(maxConcurrent) + +> Note that this API [may still change or be removed altogether](https://github.com/ReactiveX/RxJava#beta) since it is marked as `@Beta`. + +A `flatMap` overload was added that allows limiting concurrency, or the number of `Observable`s being merged . + +This now means that these are the same, one using `merge` directly, the other using `flatMap` and passing in `10` as the `maxConcurrent`: + +```java +Observable> asyncWork = range(1, 1000000) + .doOnNext(i -> System.out.println("Emitted Value: " + i)) + .map(item -> { + return just(item) + .doOnNext(MergeMaxConcurrent::sleep) + .subscribeOn(Schedulers.io()); + }); +merge(asyncWork, 10).toBlocking().forEach(v -> System.out.println("Received: " + v)); +``` + +```java +range(1, 1000000) + .doOnNext(i -> System.out.println("Emitted Value: " + i)) + .flatMap(item -> { + return just(item) + .doOnNext(MergeMaxConcurrent::sleep) + .subscribeOn(Schedulers.io()); + }, 10) + .toBlocking().forEach(v -> System.out.println("Received: " + v)); +``` + +#### Changes + +* [Pull 2627] (https://github.com/ReactiveX/RxJava/pull/2627) FlatMap overloads with maximum concurrency parameter +* [Pull 2648] (https://github.com/ReactiveX/RxJava/pull/2648) TakeWhile: don't unsubscribe downstream. +* [Pull 2580] (https://github.com/ReactiveX/RxJava/pull/2580) Allow configuring the maximum number of computation scheduler threads +* [Pull 2601] (https://github.com/ReactiveX/RxJava/pull/2601) Added common Exceptions.throwIfAny to throw a collection of exceptions +* [Pull 2644] (https://github.com/ReactiveX/RxJava/pull/2644) Missing Unsafe class yields NoClassDefFoundError +* [Pull 2642] (https://github.com/ReactiveX/RxJava/pull/2642) Fix a potential memory leak in schedulePeriodically +* [Pull 2630] (https://github.com/ReactiveX/RxJava/pull/2630) Cast back Observer to Subscriber if passed to subscribe(Observer) +* [Pull 2622] (https://github.com/ReactiveX/RxJava/pull/2622) Changed Observable.empty() into a stateless constant observable. +* [Pull 2607] (https://github.com/ReactiveX/RxJava/pull/2607) OnSubscribeRefCount - improve comments + + ### Version 1.0.5 – Feburary 3rd 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.5%7C)) ### This release includes many bug fixes along with a few new operators and enhancements. From f67f97c914e7e0c33617fd7e3d37256de56c429d Mon Sep 17 00:00:00 2001 From: David Gross Date: Wed, 11 Feb 2015 15:25:22 -0800 Subject: [PATCH 161/857] javadocs: add @since annotations to new methods; fix formatting, polish wording for new Subscriber constructor --- src/main/java/rx/Observable.java | 1 + src/main/java/rx/Subscriber.java | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 2c1c179b4c..af563c62f8 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4538,6 +4538,7 @@ public final Observable flatMap(Func1ReactiveX operators documentation: FlatMap + * @since 1.0.6 */ @Beta public final Observable flatMap(Func1> func, int maxConcurrent) { diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 4f87002f2c..0c26201552 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -46,14 +46,18 @@ protected Subscriber() { protected Subscriber(Subscriber op) { this(op, true); } + /** - * Construct a subscriber by using the other subscriber for backpressure - * and optionally sharing the underlying subscriptions list. - *

To retain the chaining of subscribers, the caller should add the - * created instance to the op via {@code add()}. + * Construct a Subscriber by using another Subscriber for backpressure and optionally sharing the + * underlying subscriptions list. + *

+ * To retain the chaining of subscribers, add the created instance to {@code op} via {@link #add}. * - * @param op the other subscriber - * @param shareSubscriptions should the subscription list in op shared with this instance? + * @param op + * the other Subscriber + * @param shareSubscriptions + * {@code true} to share the subscription list in {@code op} with this instance + * @since 1.0.6 */ protected Subscriber(Subscriber op, boolean shareSubscriptions) { this.op = op; From ab3a3a53f601cdbed91d838ff78f5a94c7834ee8 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 11 Feb 2015 18:55:48 -0800 Subject: [PATCH 162/857] Conditional logic for pull request --- gradle/buildViaTravis.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index aa8bb98824..5338c6fad2 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -1,13 +1,13 @@ #!/bin/bash # This script will build the project. -if [ "$TRAVIS_PULL_REQUEST" ]; then - echo -e 'Build Pull Request ['$TRAVIS_PULL_REQUEST'] => Branch ['$TRAVIS_BRANCH']' +if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then + echo -e "Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]" ./gradlew -Prelease.useLastTag=true build -elif [ ! "$TRAVIS_PULL_REQUEST" ] && [ "$TRAVIS_TAG" == "" ]; then +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace -elif [ ! "$TRAVIS_PULL_REQUEST" ] && [ "$TRAVIS_TAG" != "" ]; then +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace else From 7b5a3653dc33a4d4b96b1b16ee7e42e02c5eec17 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 11 Feb 2015 19:15:30 -0800 Subject: [PATCH 163/857] Delete publishViaTravis.sh --- gradle/publishViaTravis.sh | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100755 gradle/publishViaTravis.sh diff --git a/gradle/publishViaTravis.sh b/gradle/publishViaTravis.sh deleted file mode 100755 index 2fd3432b5d..0000000000 --- a/gradle/publishViaTravis.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# This script will upload to Bintray. It is intended to be conditionally executed on tagged builds. - -echo -e 'Bintray Upload Script => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' - -if [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then - echo -e 'Bintray Upload => Starting upload ...\n' - - ./gradlew -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace -else - echo 'Bintray Upload => Not a tagged build so will not upload' -fi From c34456ca572295200b6065de848495d2694ce3b6 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 11 Feb 2015 19:15:51 -0800 Subject: [PATCH 164/857] Explicit build + snapshot --- gradle/buildViaTravis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 5338c6fad2..b8243106d2 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -6,7 +6,7 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" snapshot --stacktrace + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" build snapshot --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace From bc1ed77ad777c068fe7d2731a1067c7127a4af20 Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 12 Feb 2015 10:28:46 -0800 Subject: [PATCH 165/857] javadocs: adding (+ correcting one) @since annotations --- src/main/java/rx/Observable.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index af563c62f8..3520f5d808 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4538,7 +4538,7 @@ public final Observable flatMap(Func1ReactiveX operators documentation: FlatMap - * @since 1.0.6 + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta public final Observable flatMap(Func1> func, int maxConcurrent) { @@ -4601,6 +4601,7 @@ public final Observable flatMap( * @return an Observable that emits the results of merging the Observables returned from applying the * specified functions to the emissions and notifications of the source Observable * @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 flatMap( @@ -4662,6 +4663,7 @@ public final Observable flatMap(final Func1ReactiveX operators documentation: FlatMap + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Beta public final Observable flatMap(final Func1> collectionSelector, From c1d4ca7ecbd2da2c093aeb512668d6653a7a7609 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 13 Feb 2015 21:46:39 +0100 Subject: [PATCH 166/857] SwitchOnNext: fixed wrong producer --- .../rx/internal/operators/OperatorMerge.java | 39 +++++++------ .../rx/internal/operators/OperatorSwitch.java | 5 +- .../operators/OperatorSwitchTest.java | 56 +++++++++++++++++-- 3 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 2da1844ca9..dfec24d061 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -194,7 +194,7 @@ private void handleNewSource(Observable t) { } MergeProducer producerIfNeeded = null; // if we have received a request then we need to respect it, otherwise we fast-path - if (mergeProducer.requested != Long.MAX_VALUE) { + if (mergeProducer.requested() != Long.MAX_VALUE) { /** *

 {@code
                  * With this optimization:
@@ -237,7 +237,7 @@ private void handleScalarSynchronousObservable(ScalarSynchronousObservable
              * 
              */
-            if (mergeProducer.requested == Long.MAX_VALUE) {
+            if (mergeProducer.requested() == Long.MAX_VALUE) {
                 handleScalarSynchronousObservableWithoutRequestLimits(t);
             } else {
                 handleScalarSynchronousObservableWithRequestLimits(t);
@@ -274,11 +274,11 @@ private void handleScalarSynchronousObservableWithRequestLimits(ScalarSynchronou
                 boolean moreToDrain;
                 boolean isReturn = false;
                 try {
-                    long r = mergeProducer.requested;
+                    long r = mergeProducer.requested();
                     if (r > 0) {
                         emitted = true;
                         actual.onNext(t.get());
-                        MergeProducer.REQUESTED.decrementAndGet(mergeProducer);
+                        mergeProducer.getAndAdd(-1);
                         // we handle this Observable without ever incrementing the wip or touching other machinery so just return here
                         isReturn = true;
                     }
@@ -376,7 +376,7 @@ private void drainChildrenQueues() {
         private int drainScalarValueQueue() {
             RxRingBuffer svq = scalarValueQueue;
             if (svq != null) {
-                long r = mergeProducer.requested;
+                long r = mergeProducer.requested();
                 int emittedWhileDraining = 0;
                 if (r < 0) {
                     // drain it all
@@ -398,7 +398,7 @@ private int drainScalarValueQueue() {
                         }
                     }
                     // decrement the number we emitted from outstanding requests
-                    MergeProducer.REQUESTED.getAndAdd(mergeProducer, -emittedWhileDraining);
+                    mergeProducer.getAndAdd(-emittedWhileDraining);
                 }
                 return emittedWhileDraining;
             }
@@ -410,7 +410,7 @@ private int drainScalarValueQueue() {
             @Override
             public Boolean call(InnerSubscriber s) {
                 if (s.q != null) {
-                    long r = mergeProducer.requested;
+                    long r = mergeProducer.requested();
                     int emitted = s.drainQueue();
                     if (emitted > 0) {
                         s.requestMore(emitted);
@@ -533,19 +533,26 @@ public MergeProducer(MergeSubscriber ms) {
             this.ms = ms;
         }
 
-        private volatile long requested = 0;
+        private volatile long rq = 0;
         @SuppressWarnings("rawtypes")
-        static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(MergeProducer.class, "requested");
+        static final AtomicLongFieldUpdater RQ = AtomicLongFieldUpdater.newUpdater(MergeProducer.class, "rq");
 
+        public long requested() {
+            return rq;
+        }
+        public long getAndAdd(long n) {
+            return RQ.getAndAdd(this, n);
+        }
+        
         @Override
         public void request(long n) {
-            if (requested == Long.MAX_VALUE) {
+            if (rq == Long.MAX_VALUE) {
                 return;
             }
             if (n == Long.MAX_VALUE) {
-                requested = Long.MAX_VALUE;
+                rq = Long.MAX_VALUE;
             } else {
-                BackpressureUtils.getAndAddRequest(REQUESTED, this, n);
+                BackpressureUtils.getAndAddRequest(RQ, this, n);
                 if (ms.drainQueuesIfNeeded()) {
                     boolean sendComplete = false;
                     synchronized (ms) {
@@ -668,7 +675,7 @@ private void emit(T t, boolean complete) {
                     } else {
                         // this needs to check q.count() as draining above may not have drained the full queue
                         // perf tests show this to be okay, though different queue implementations could perform poorly with this
-                        if (producer.requested > 0 && q.count() == 0) {
+                        if (producer.requested() > 0 && q.count() == 0) {
                             if (complete) {
                                 parentSubscriber.completeInner(this);
                             } else {
@@ -679,7 +686,7 @@ private void emit(T t, boolean complete) {
                                     onError(OnErrorThrowable.addValueAsLastCause(e, t));
                                 }
                                 emitted++;
-                                MergeProducer.REQUESTED.decrementAndGet(producer);
+                                producer.getAndAdd(-1);
                             }
                         } else {
                             // no requests available, so enqueue it
@@ -728,7 +735,7 @@ private void enqueue(T t, boolean complete) {
         private int drainRequested() {
             int emitted = 0;
             // drain what was requested
-            long toEmit = producer.requested;
+            long toEmit = producer.requested();
             Object o;
             for (int i = 0; i < toEmit; i++) {
                 o = q.poll();
@@ -750,7 +757,7 @@ private int drainRequested() {
             }
 
             // decrement the number we emitted from outstanding requests
-            MergeProducer.REQUESTED.getAndAdd(producer, -emitted);
+            producer.getAndAdd(-emitted);
             return emitted;
         }
 
diff --git a/src/main/java/rx/internal/operators/OperatorSwitch.java b/src/main/java/rx/internal/operators/OperatorSwitch.java
index 7ee71084aa..eae4d3aa67 100644
--- a/src/main/java/rx/internal/operators/OperatorSwitch.java
+++ b/src/main/java/rx/internal/operators/OperatorSwitch.java
@@ -49,7 +49,9 @@ public static  OperatorSwitch instance() {
     private OperatorSwitch() { }
     @Override
     public Subscriber> call(final Subscriber child) {
-        return new SwitchSubscriber(child);
+        SwitchSubscriber sws = new SwitchSubscriber(child);
+        child.add(sws);
+        return sws;
     }
 
     private static final class SwitchSubscriber extends Subscriber> {
@@ -75,7 +77,6 @@ private static final class SwitchSubscriber extends Subscriber child) {
-            super(child);
             s = new SerializedSubscriber(child);
             ssub = new SerialSubscription();
             child.add(ssub);
diff --git a/src/test/java/rx/internal/operators/OperatorSwitchTest.java b/src/test/java/rx/internal/operators/OperatorSwitchTest.java
index a3a05f3b37..0efc388db1 100644
--- a/src/test/java/rx/internal/operators/OperatorSwitchTest.java
+++ b/src/test/java/rx/internal/operators/OperatorSwitchTest.java
@@ -18,23 +18,25 @@
 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 static org.mockito.Mockito.*;
 
 import java.util.Arrays;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 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.Func1;
 import rx.observers.TestSubscriber;
 import rx.schedulers.TestScheduler;
 
@@ -530,4 +532,46 @@ public void call(final Subscriber> subscriber) {
         ).take(1).subscribe();
         assertTrue("Switch doesn't propagate 'unsubscribe'", isUnsubscribed.get());
     }
+    /** The upstream producer hijacked the switch producer stopping the requests aimed at the inner observables. */
+    @Test
+    public void testIssue2654() {
+        Observable oneItem = Observable.just("Hello").mergeWith(Observable.never());
+        
+        Observable src = oneItem.switchMap(new Func1>() {
+            @Override
+            public Observable call(final String s) {
+                return Observable.just(s)
+                        .mergeWith(Observable.interval(10, TimeUnit.MILLISECONDS)
+                        .map(new Func1() {
+                            @Override
+                            public String call(Long i) {
+                                return s + " " + i;
+                            }
+                        })).take(250);
+            }
+        })
+        .share()
+        ;
+        
+        TestSubscriber ts = new TestSubscriber() {
+            @Override
+            public void onNext(String t) {
+                super.onNext(t);
+                if (getOnNextEvents().size() == 250) {
+                    onCompleted();
+                    unsubscribe();
+                }
+            }
+        };
+        src.subscribe(ts);
+        
+        ts.awaitTerminalEvent(10, TimeUnit.SECONDS);
+        
+        System.out.println("> testIssue2654: " + ts.getOnNextEvents().size());
+        
+        ts.assertTerminalEvent();
+        ts.assertNoErrors();
+        
+        Assert.assertEquals(250, ts.getOnNextEvents().size());
+    }
 }

From 2c2051f53d571320b58787f9110b0bed687ec2b5 Mon Sep 17 00:00:00 2001
From: akarnokd 
Date: Fri, 13 Feb 2015 21:50:29 +0100
Subject: [PATCH 167/857] Restore merge changes: not related to the bug

---
 .../rx/internal/operators/OperatorMerge.java  | 39 ++++++++-----------
 1 file changed, 16 insertions(+), 23 deletions(-)

diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java
index dfec24d061..2da1844ca9 100644
--- a/src/main/java/rx/internal/operators/OperatorMerge.java
+++ b/src/main/java/rx/internal/operators/OperatorMerge.java
@@ -194,7 +194,7 @@ private void handleNewSource(Observable t) {
             }
             MergeProducer producerIfNeeded = null;
             // if we have received a request then we need to respect it, otherwise we fast-path
-            if (mergeProducer.requested() != Long.MAX_VALUE) {
+            if (mergeProducer.requested != Long.MAX_VALUE) {
                 /**
                  * 
 {@code
                  * With this optimization:
@@ -237,7 +237,7 @@ private void handleScalarSynchronousObservable(ScalarSynchronousObservable
              * 
              */
-            if (mergeProducer.requested() == Long.MAX_VALUE) {
+            if (mergeProducer.requested == Long.MAX_VALUE) {
                 handleScalarSynchronousObservableWithoutRequestLimits(t);
             } else {
                 handleScalarSynchronousObservableWithRequestLimits(t);
@@ -274,11 +274,11 @@ private void handleScalarSynchronousObservableWithRequestLimits(ScalarSynchronou
                 boolean moreToDrain;
                 boolean isReturn = false;
                 try {
-                    long r = mergeProducer.requested();
+                    long r = mergeProducer.requested;
                     if (r > 0) {
                         emitted = true;
                         actual.onNext(t.get());
-                        mergeProducer.getAndAdd(-1);
+                        MergeProducer.REQUESTED.decrementAndGet(mergeProducer);
                         // we handle this Observable without ever incrementing the wip or touching other machinery so just return here
                         isReturn = true;
                     }
@@ -376,7 +376,7 @@ private void drainChildrenQueues() {
         private int drainScalarValueQueue() {
             RxRingBuffer svq = scalarValueQueue;
             if (svq != null) {
-                long r = mergeProducer.requested();
+                long r = mergeProducer.requested;
                 int emittedWhileDraining = 0;
                 if (r < 0) {
                     // drain it all
@@ -398,7 +398,7 @@ private int drainScalarValueQueue() {
                         }
                     }
                     // decrement the number we emitted from outstanding requests
-                    mergeProducer.getAndAdd(-emittedWhileDraining);
+                    MergeProducer.REQUESTED.getAndAdd(mergeProducer, -emittedWhileDraining);
                 }
                 return emittedWhileDraining;
             }
@@ -410,7 +410,7 @@ private int drainScalarValueQueue() {
             @Override
             public Boolean call(InnerSubscriber s) {
                 if (s.q != null) {
-                    long r = mergeProducer.requested();
+                    long r = mergeProducer.requested;
                     int emitted = s.drainQueue();
                     if (emitted > 0) {
                         s.requestMore(emitted);
@@ -533,26 +533,19 @@ public MergeProducer(MergeSubscriber ms) {
             this.ms = ms;
         }
 
-        private volatile long rq = 0;
+        private volatile long requested = 0;
         @SuppressWarnings("rawtypes")
-        static final AtomicLongFieldUpdater RQ = AtomicLongFieldUpdater.newUpdater(MergeProducer.class, "rq");
+        static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(MergeProducer.class, "requested");
 
-        public long requested() {
-            return rq;
-        }
-        public long getAndAdd(long n) {
-            return RQ.getAndAdd(this, n);
-        }
-        
         @Override
         public void request(long n) {
-            if (rq == Long.MAX_VALUE) {
+            if (requested == Long.MAX_VALUE) {
                 return;
             }
             if (n == Long.MAX_VALUE) {
-                rq = Long.MAX_VALUE;
+                requested = Long.MAX_VALUE;
             } else {
-                BackpressureUtils.getAndAddRequest(RQ, this, n);
+                BackpressureUtils.getAndAddRequest(REQUESTED, this, n);
                 if (ms.drainQueuesIfNeeded()) {
                     boolean sendComplete = false;
                     synchronized (ms) {
@@ -675,7 +668,7 @@ private void emit(T t, boolean complete) {
                     } else {
                         // this needs to check q.count() as draining above may not have drained the full queue
                         // perf tests show this to be okay, though different queue implementations could perform poorly with this
-                        if (producer.requested() > 0 && q.count() == 0) {
+                        if (producer.requested > 0 && q.count() == 0) {
                             if (complete) {
                                 parentSubscriber.completeInner(this);
                             } else {
@@ -686,7 +679,7 @@ private void emit(T t, boolean complete) {
                                     onError(OnErrorThrowable.addValueAsLastCause(e, t));
                                 }
                                 emitted++;
-                                producer.getAndAdd(-1);
+                                MergeProducer.REQUESTED.decrementAndGet(producer);
                             }
                         } else {
                             // no requests available, so enqueue it
@@ -735,7 +728,7 @@ private void enqueue(T t, boolean complete) {
         private int drainRequested() {
             int emitted = 0;
             // drain what was requested
-            long toEmit = producer.requested();
+            long toEmit = producer.requested;
             Object o;
             for (int i = 0; i < toEmit; i++) {
                 o = q.poll();
@@ -757,7 +750,7 @@ private int drainRequested() {
             }
 
             // decrement the number we emitted from outstanding requests
-            producer.getAndAdd(-emitted);
+            MergeProducer.REQUESTED.getAndAdd(producer, -emitted);
             return emitted;
         }
 

From 758fe33b8d6031209a39fe5f2a87cd27240a443c Mon Sep 17 00:00:00 2001
From: akarnokd 
Date: Thu, 19 Feb 2015 10:22:24 +0100
Subject: [PATCH 168/857] Operator WithLatestFrom

---
 src/main/java/rx/Observable.java              |  24 ++
 .../operators/OperatorWithLatestFrom.java     | 103 ++++++
 .../rx/observers/SerializedSubscriber.java    |  12 +-
 .../operators/OperatorWithLatestFromTest.java | 298 ++++++++++++++++++
 4 files changed, 436 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/rx/internal/operators/OperatorWithLatestFrom.java
 create mode 100644 src/test/java/rx/internal/operators/OperatorWithLatestFromTest.java

diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java
index 3520f5d808..9c602bdd07 100644
--- a/src/main/java/rx/Observable.java
+++ b/src/main/java/rx/Observable.java
@@ -8769,6 +8769,30 @@ public final Observable unsubscribeOn(Scheduler scheduler) {
         return lift(new OperatorUnsubscribeOn(scheduler));
     }
 
+    /**
+     * Merges the specified observable sequence into this Observable sequence by using the resultSelector 
+     * function only when the source observable sequence (this instance) produces an element.
+     * 
+     * ----A-------B------C----->  o1
+     *
+     * --0----1-2----3-4-------->  o2
+     *
+     *     |       |      |
+     *     V       V      V
+     *
+     *   (A,0)   (B,2)  (C,4)
+     * 
+ * @param other the other observable sequence + * @param resultSelector the function to call when this Observable emits an element and the other + * observable sequence has already emitted a value. + * @return an Observable that merges the specified observable sequence into this Observable sequence + * by using the resultSelector function only when the source observable sequence + * (this instance) produces an element + */ + public final Observable withLatestFrom(Observable other, Func2 resultSelector) { + return lift(new OperatorWithLatestFrom(other, resultSelector)); + } + /** * Returns an Observable that emits windows of items it collects from the source Observable. The resulting * Observable emits connected, non-overlapping windows. It emits the current window and opens a new one diff --git a/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java b/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java new file mode 100644 index 0000000000..4bf610b6b1 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java @@ -0,0 +1,103 @@ +/** + * 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.atomic.AtomicReference; + +import rx.*; +import rx.Observable.Operator; +import rx.functions.Func2; +import rx.observers.SerializedSubscriber; + +/** + * Combines values from two sources only when the main source emits. + * @param the element type of the main observable + * @param the element type of the other observable that is merged into the main + * @param the result element type + */ +public final class OperatorWithLatestFrom implements Operator { + final Func2 resultSelector; + final Observable other; + /** Indicates the other has not yet emitted a value. */ + static final Object EMPTY = new Object(); + + public OperatorWithLatestFrom(Observable other, Func2 resultSelector) { + this.other = other; + this.resultSelector = resultSelector; + } + @Override + public Subscriber call(Subscriber child) { + // onError and onCompleted may happen either from the main or from other. + final SerializedSubscriber s = new SerializedSubscriber(child, false); + child.add(s); + + final AtomicReference current = new AtomicReference(EMPTY); + + final Subscriber subscriber = new Subscriber(s, true) { + @Override + public void onNext(T t) { + Object o = current.get(); + if (o != EMPTY) { + try { + @SuppressWarnings("unchecked") + U u = (U)o; + R result = resultSelector.call(t, u); + + s.onNext(result); + } catch (Throwable e) { + onError(e); + return; + } + } + } + @Override + public void onError(Throwable e) { + s.onError(e); + s.unsubscribe(); + } + @Override + public void onCompleted() { + s.onCompleted(); + s.unsubscribe(); + } + }; + + Subscriber otherSubscriber = new Subscriber() { + @Override + public void onNext(U t) { + current.set(t); + } + @Override + public void onError(Throwable e) { + s.onError(e); + s.unsubscribe(); + } + @Override + public void onCompleted() { + if (current.get() == EMPTY) { + s.onCompleted(); + s.unsubscribe(); + } + } + }; + s.add(subscriber); + s.add(otherSubscriber); + + other.unsafeSubscribe(otherSubscriber); + + return subscriber; + } +} diff --git a/src/main/java/rx/observers/SerializedSubscriber.java b/src/main/java/rx/observers/SerializedSubscriber.java index c9bde2b6aa..e277506bcd 100644 --- a/src/main/java/rx/observers/SerializedSubscriber.java +++ b/src/main/java/rx/observers/SerializedSubscriber.java @@ -37,7 +37,17 @@ public class SerializedSubscriber extends Subscriber { private final Observer s; public SerializedSubscriber(Subscriber s) { - super(s); + this(s, true); + } + /** + * Constructor for wrapping and serializing a subscriber optionally sharing the same underlying subscription + * list. + * @param s the subscriber to wrap and serialize + * @param shareSubscriptions if {@code true}, the same subscription list is shared between this + * subscriber and {@code s}. + */ + public SerializedSubscriber(Subscriber s, boolean shareSubscriptions) { + super(s, shareSubscriptions); this.s = new SerializedObserver(s); } diff --git a/src/test/java/rx/internal/operators/OperatorWithLatestFromTest.java b/src/test/java/rx/internal/operators/OperatorWithLatestFromTest.java new file mode 100644 index 0000000000..a172158115 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorWithLatestFromTest.java @@ -0,0 +1,298 @@ +/** + * 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.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import rx.Observable; +import rx.Observer; +import rx.exceptions.TestException; +import rx.functions.Func2; +import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; + +public class OperatorWithLatestFromTest { + static final Func2 COMBINER = new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return (t1 << 8) + t2; + } + }; + static final Func2 COMBINER_ERROR = new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + throw new TestException("Forced failure"); + } + }; + @Test + public void testSimple() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + Observable result = source.withLatestFrom(other, COMBINER); + + result.subscribe(o); + + source.onNext(1); + inOrder.verify(o, never()).onNext(anyInt()); + + other.onNext(1); + inOrder.verify(o, never()).onNext(anyInt()); + + source.onNext(2); + inOrder.verify(o).onNext((2 << 8) + 1); + + other.onNext(2); + inOrder.verify(o, never()).onNext(anyInt()); + + other.onCompleted(); + inOrder.verify(o, never()).onCompleted(); + + source.onNext(3); + inOrder.verify(o).onNext((3 << 8) + 2); + + source.onCompleted(); + inOrder.verify(o).onCompleted(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void testEmptySource() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER); + + TestSubscriber ts = new TestSubscriber(); + + result.subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + + source.onCompleted(); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + assertEquals(0, ts.getOnNextEvents().size()); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void testEmptyOther() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER); + + TestSubscriber ts = new TestSubscriber(); + + result.subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + source.onNext(1); + + source.onCompleted(); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + assertEquals(0, ts.getOnNextEvents().size()); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + + @Test + public void testUnsubscription() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER); + + TestSubscriber ts = new TestSubscriber(); + + result.subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + ts.unsubscribe(); + + ts.assertReceivedOnNext(Arrays.asList((1 << 8) + 1)); + ts.assertNoErrors(); + assertEquals(0, ts.getOnCompletedEvents().size()); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void testSourceThrows() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER); + + TestSubscriber ts = new TestSubscriber(); + + result.subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + source.onError(new TestException()); + + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList((1 << 8) + 1)); + assertEquals(1, ts.getOnErrorEvents().size()); + assertTrue(ts.getOnErrorEvents().get(0) instanceof TestException); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + @Test + public void testOtherThrows() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER); + + TestSubscriber ts = new TestSubscriber(); + + result.subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + other.onError(new TestException()); + + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList((1 << 8) + 1)); + assertEquals(1, ts.getOnErrorEvents().size()); + assertTrue(ts.getOnErrorEvents().get(0) instanceof TestException); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void testFunctionThrows() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER_ERROR); + + TestSubscriber ts = new TestSubscriber(); + + result.subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + ts.assertTerminalEvent(); + assertEquals(0, ts.getOnNextEvents().size()); + assertEquals(1, ts.getOnErrorEvents().size()); + assertTrue(ts.getOnErrorEvents().get(0) instanceof TestException); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void testNoDownstreamUnsubscribe() { + PublishSubject source = PublishSubject.create(); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER); + + TestSubscriber ts = new TestSubscriber(); + + result.unsafeSubscribe(ts); + + source.onCompleted(); + + assertFalse(ts.isUnsubscribed()); + } + @Test + public void testBackpressure() { + Observable source = Observable.range(1, 10); + PublishSubject other = PublishSubject.create(); + + Observable result = source.withLatestFrom(other, COMBINER); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(0); + } + }; + + result.subscribe(ts); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Collections.emptyList()); + + other.onNext(1); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Arrays.asList((2 << 8) + 1)); + + ts.requestMore(5); + ts.assertReceivedOnNext(Arrays.asList( + (2 << 8) + 1, (3 << 8) + 1, (4 << 8) + 1, (5 << 8) + 1, + (6 << 8) + 1, (7 << 8) + 1 + )); + + ts.unsubscribe(); + + assertFalse("Other has observers!", other.hasObservers()); + + ts.assertNoErrors(); + } +} From 844cc95842cbdcfc54fd00a87f8f4e8f5b552312 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 19 Feb 2015 13:43:47 +0100 Subject: [PATCH 169/857] Experimental annotation. --- src/main/java/rx/Observable.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 9c602bdd07..b2987239e5 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -8789,6 +8789,7 @@ public final Observable unsubscribeOn(Scheduler scheduler) { * by using the resultSelector function only when the source observable sequence * (this instance) produces an element */ + @Experimental public final Observable withLatestFrom(Observable other, Func2 resultSelector) { return lift(new OperatorWithLatestFrom(other, resultSelector)); } From 56eb4bd253c5a7a7211bb984700eb4515107e857 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 20 Feb 2015 10:52:38 +0100 Subject: [PATCH 170/857] Optimized isUnsubscribed check --- .../rx/internal/util/SubscriptionList.java | 70 ++++++++------ .../subscriptions/CompositeSubscription.java | 93 +++++++++++-------- 2 files changed, 98 insertions(+), 65 deletions(-) diff --git a/src/main/java/rx/internal/util/SubscriptionList.java b/src/main/java/rx/internal/util/SubscriptionList.java index 02aecdef71..7583131fda 100644 --- a/src/main/java/rx/internal/util/SubscriptionList.java +++ b/src/main/java/rx/internal/util/SubscriptionList.java @@ -32,7 +32,7 @@ public final class SubscriptionList implements Subscription { private List subscriptions; - private boolean unsubscribed = false; + private volatile boolean unsubscribed; public SubscriptionList() { } @@ -42,7 +42,7 @@ public SubscriptionList(final Subscription... subscriptions) { } @Override - public synchronized boolean isUnsubscribed() { + public boolean isUnsubscribed() { return unsubscribed; } @@ -55,21 +55,19 @@ public synchronized boolean isUnsubscribed() { * the {@link Subscription} to add */ public void add(final Subscription s) { - Subscription unsubscribe = null; - synchronized (this) { - if (unsubscribed) { - unsubscribe = s; - } else { - if (subscriptions == null) { - subscriptions = new LinkedList(); + if (!unsubscribed) { + synchronized (this) { + if (!unsubscribed) { + if (subscriptions == null) { + subscriptions = new LinkedList(); + } + subscriptions.add(s); + return; } - subscriptions.add(s); } } - if (unsubscribe != null) { - // call after leaving the synchronized block so we're not holding a lock while executing this - unsubscribe.unsubscribe(); - } + // call after leaving the synchronized block so we're not holding a lock while executing this + s.unsubscribe(); } /** @@ -78,17 +76,19 @@ public void add(final Subscription s) { */ @Override public void unsubscribe() { - List list; - synchronized (this) { - if (unsubscribed) { - return; + if (!unsubscribed) { + List list; + synchronized (this) { + if (unsubscribed) { + return; + } + unsubscribed = true; + list = subscriptions; + subscriptions = null; } - unsubscribed = true; - list = subscriptions; - subscriptions = null; + // we will only get here once + unsubscribeFromAll(list); } - // we will only get here once - unsubscribeFromAll(list); } private static void unsubscribeFromAll(Collection subscriptions) { @@ -110,11 +110,25 @@ private static void unsubscribeFromAll(Collection subscriptions) { } /* perf support */ public void clear() { - List list; - synchronized (this) { - list = subscriptions; - subscriptions = null; + if (!unsubscribed) { + List list; + synchronized (this) { + list = subscriptions; + subscriptions = null; + } + unsubscribeFromAll(list); + } + } + /** + * Returns true if this composite is not unsubscribed and contains subscriptions. + * @return {@code true} if this composite is not unsubscribed and contains subscriptions. + */ + public boolean hasSubscriptions() { + if (!unsubscribed) { + synchronized (this) { + return !unsubscribed && subscriptions != null && !subscriptions.isEmpty(); + } } - unsubscribeFromAll(list); + return false; } } diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index 3c8124f3ec..b4d82ce092 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -31,7 +31,7 @@ public final class CompositeSubscription implements Subscription { private Set subscriptions; - private boolean unsubscribed = false; + private volatile boolean unsubscribed; public CompositeSubscription() { } @@ -41,7 +41,7 @@ public CompositeSubscription(final Subscription... subscriptions) { } @Override - public synchronized boolean isUnsubscribed() { + public boolean isUnsubscribed() { return unsubscribed; } @@ -58,21 +58,19 @@ public void add(final Subscription s) { if (s.isUnsubscribed()) { return; } - Subscription unsubscribe = null; - synchronized (this) { - if (unsubscribed) { - unsubscribe = s; - } else { - if (subscriptions == null) { - subscriptions = new HashSet(4); + if (!unsubscribed) { + synchronized (this) { + if (!unsubscribed) { + if (subscriptions == null) { + subscriptions = new HashSet(4); + } + subscriptions.add(s); + return; } - subscriptions.add(s); } } - if (unsubscribe != null) { - // call after leaving the synchronized block so we're not holding a lock while executing this - unsubscribe.unsubscribe(); - } + // call after leaving the synchronized block so we're not holding a lock while executing this + s.unsubscribe(); } /** @@ -83,16 +81,18 @@ public void add(final Subscription s) { * the {@link Subscription} to remove */ public void remove(final Subscription s) { - boolean unsubscribe = false; - synchronized (this) { - if (unsubscribed || subscriptions == null) { - return; + if (!unsubscribed) { + boolean unsubscribe = false; + synchronized (this) { + if (unsubscribed || subscriptions == null) { + return; + } + unsubscribe = subscriptions.remove(s); + } + if (unsubscribe) { + // if we removed successfully we then need to call unsubscribe on it (outside of the lock) + s.unsubscribe(); } - unsubscribe = subscriptions.remove(s); - } - if (unsubscribe) { - // if we removed successfully we then need to call unsubscribe on it (outside of the lock) - s.unsubscribe(); } } @@ -102,28 +102,35 @@ public void remove(final Subscription s) { * an unoperative state. */ public void clear() { - Collection unsubscribe = null; - synchronized (this) { - if (unsubscribed || subscriptions == null) { - return; - } else { - unsubscribe = subscriptions; - subscriptions = null; + if (!unsubscribed) { + Collection unsubscribe = null; + synchronized (this) { + if (unsubscribed || subscriptions == null) { + return; + } else { + unsubscribe = subscriptions; + subscriptions = null; + } } + unsubscribeFromAll(unsubscribe); } - unsubscribeFromAll(unsubscribe); } @Override public void unsubscribe() { - synchronized (this) { - if (unsubscribed) { - return; + if (!unsubscribed) { + Collection unsubscribe = null; + synchronized (this) { + if (unsubscribed) { + return; + } + unsubscribed = true; + unsubscribe = subscriptions; + subscriptions = null; } - unsubscribed = true; + // we will only get here once + unsubscribeFromAll(unsubscribe); } - // we will only get here once - unsubscribeFromAll(subscriptions); } private static void unsubscribeFromAll(Collection subscriptions) { @@ -143,4 +150,16 @@ private static void unsubscribeFromAll(Collection subscriptions) { } Exceptions.throwIfAny(es); } + /** + * Returns true if this composite is not unsubscribed and contains subscriptions. + * @return {@code true} if this composite is not unsubscribed and contains subscriptions. + */ + public boolean hasSubscriptions() { + if (!unsubscribed) { + synchronized (this) { + return !unsubscribed && subscriptions != null && !subscriptions.isEmpty(); + } + } + return false; + } } From ffe0fa1c7e6f38a668d1650c1409a9eb81604788 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 19 Feb 2015 15:57:50 +1100 Subject: [PATCH 171/857] using should use unsafeSubscribe, allow eager disposal to enable the synchronous case --- src/main/java/rx/Observable.java | 39 ++- .../internal/operators/OnSubscribeUsing.java | 100 ++++++-- .../operators/OnSubscribeUsingTest.java | 227 +++++++++++++++++- 3 files changed, 338 insertions(+), 28 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 3520f5d808..4ae2bfa3d3 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -2547,7 +2547,7 @@ public final static Observable timer(long delay, TimeUnit unit, Scheduler } /** - * Constructs an Observable that creates a dependent resource object. + * Constructs an Observable that creates a dependent resource object which is disposed of on unsubscription. *

* *

@@ -2568,7 +2568,42 @@ public final static Observable using( final Func0 resourceFactory, final Func1> observableFactory, final Action1 disposeAction) { - return create(new OnSubscribeUsing(resourceFactory, observableFactory, disposeAction)); + return using(resourceFactory, observableFactory, disposeAction, false); + } + + /** + * Constructs an Observable that creates a dependent resource object which is disposed of just before + * termination if disposeEagerly is set to 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. disposeAction will only be called once per subscription. + *

+ * + *

+ *
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 Observable + * @param observableFactory + * the factory function to create an Observable + * @param disposeAction + * the function that will dispose of the resource + * @param disposeEagerly + * if true then disposal will happen either on unsubscription or just before emission of + * a terminal event (onComplete or onError). + * @return the Observable 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 final static Observable using( + final Func0 resourceFactory, + final Func1> observableFactory, + final Action1 disposeAction, boolean disposeEagerly) { + return create(new OnSubscribeUsing(resourceFactory, observableFactory, disposeAction, disposeEagerly)); } /** diff --git a/src/main/java/rx/internal/operators/OnSubscribeUsing.java b/src/main/java/rx/internal/operators/OnSubscribeUsing.java index f4a1bc0391..8c29d632d9 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeUsing.java +++ b/src/main/java/rx/internal/operators/OnSubscribeUsing.java @@ -15,14 +15,18 @@ */ package rx.internal.operators; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; +import rx.Subscription; +import rx.exceptions.CompositeException; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func0; import rx.functions.Func1; -import rx.subscriptions.Subscriptions; /** * Constructs an observable sequence that depends on a resource object. @@ -32,35 +36,103 @@ public final class OnSubscribeUsing implements OnSubscribe { private final Func0 resourceFactory; private final Func1> observableFactory; private final Action1 dispose; + private final boolean disposeEagerly; public OnSubscribeUsing(Func0 resourceFactory, Func1> observableFactory, - Action1 dispose) { + Action1 dispose, boolean disposeEagerly) { this.resourceFactory = resourceFactory; this.observableFactory = observableFactory; this.dispose = dispose; + this.disposeEagerly = disposeEagerly; } @Override public void call(Subscriber subscriber) { - try { - final Resource resource = resourceFactory.call(); - subscriber.add(Subscriptions.create(new Action0() { - @Override - public void call() { - dispose.call(resource); - } + try { - })); - Observable observable = observableFactory.call(resource); - observable.subscribe(subscriber); + // create the resource + final Resource resource = resourceFactory.call(); + // create an action/subscription that disposes only once + final DisposeAction disposeOnceOnly = new DisposeAction(dispose, + resource); + // dispose on unsubscription + subscriber.add(disposeOnceOnly); + // create the observable + final Observable source = observableFactory + // create the observable + .call(resource); + final Observable observable; + // supplement with on termination disposal if requested + if (disposeEagerly) + observable = source + // dispose on completion or error + .doOnTerminate(disposeOnceOnly); + else + observable = source; + try { + // start + observable.unsafeSubscribe(subscriber); + } catch (Throwable e) { + Throwable disposeError = disposeEagerlyIfRequested(disposeOnceOnly); + if (disposeError != null) + subscriber.onError(new CompositeException(Arrays.asList(e, disposeError))); + else + // propagate error + subscriber.onError(e); + } } catch (Throwable e) { - // eagerly call unsubscribe since this operator is specifically about resource management - subscriber.unsubscribe(); // then propagate error subscriber.onError(e); } } + private Throwable disposeEagerlyIfRequested(final Action0 disposeOnceOnly) { + if (disposeEagerly) + try { + disposeOnceOnly.call(); + return null; + } catch (Throwable e) { + return e; + } + else + return null; + } + + private static final class DisposeAction extends AtomicBoolean implements Action0, + Subscription { + private static final long serialVersionUID = 4262875056400218316L; + + private Action1 dispose; + private Resource resource; + + private DisposeAction(Action1 dispose, Resource resource) { + this.dispose = dispose; + this.resource = resource; + lazySet(false); // StoreStore barrier + } + + @Override + public void call() { + if (compareAndSet(false, true)) { + try { + dispose.call(resource); + } finally { + resource = null; + dispose = null; + } + } + } + + @Override + public boolean isUnsubscribed() { + return get(); + } + + @Override + public void unsubscribe() { + call(); + } + } } diff --git a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java index ee541fbc7f..c66e43e246 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java @@ -15,6 +15,7 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -22,6 +23,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import org.junit.Test; import org.mockito.InOrder; @@ -53,18 +58,27 @@ public void call(Resource r) { } } - + private final Action1 disposeSubscription = new Action1() { @Override public void call(Subscription s) { s.unsubscribe(); } - + }; @Test public void testUsing() { + performTestUsing(false); + } + + @Test + public void testUsingEagerly() { + performTestUsing(true); + } + + private void performTestUsing(boolean disposeEagerly) { final Resource resource = mock(Resource.class); when(resource.getTextFromWeb()).thenReturn("Hello world!"); @@ -84,7 +98,8 @@ public Observable call(Resource resource) { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); - Observable observable = Observable.using(resourceFactory, observableFactory, new DisposeAction()); + Observable observable = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); observable.subscribe(observer); InOrder inOrder = inOrder(observer); @@ -99,6 +114,15 @@ public Observable call(Resource resource) { @Test public void testUsingWithSubscribingTwice() { + performTestUsingWithSubscribingTwice(false); + } + + @Test + public void testUsingWithSubscribingTwiceDisposeEagerly() { + performTestUsingWithSubscribingTwice(true); + } + + private void performTestUsingWithSubscribingTwice(boolean disposeEagerly) { // When subscribe is called, a new resource should be created. Func0 resourceFactory = new Func0() { @Override @@ -115,7 +139,7 @@ public String getTextFromWeb() { } return "Nothing"; } - + @Override public void dispose() { // do nothing @@ -134,7 +158,8 @@ public Observable call(Resource resource) { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); - Observable observable = Observable.using(resourceFactory, observableFactory, new DisposeAction()); + Observable observable = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); observable.subscribe(observer); observable.subscribe(observer); @@ -152,6 +177,15 @@ public Observable call(Resource resource) { @Test(expected = TestException.class) public void testUsingWithResourceFactoryError() { + performTestUsingWithResourceFactoryError(false); + } + + @Test(expected = TestException.class) + public void testUsingWithResourceFactoryErrorDisposeEagerly() { + performTestUsingWithResourceFactoryError(true); + } + + private void performTestUsingWithResourceFactoryError(boolean disposeEagerly) { Func0 resourceFactory = new Func0() { @Override public Subscription call() { @@ -165,12 +199,22 @@ public Observable call(Subscription subscription) { return Observable.empty(); } }; - - Observable.using(resourceFactory, observableFactory, disposeSubscription).toBlocking().last(); + + Observable.using(resourceFactory, observableFactory, disposeSubscription).toBlocking() + .last(); } @Test public void testUsingWithObservableFactoryError() { + performTestUsingWithObservableFactoryError(false); + } + + @Test + public void testUsingWithObservableFactoryErrorDisposeEagerly() { + performTestUsingWithObservableFactoryError(true); + } + + private void performTestUsingWithObservableFactoryError(boolean disposeEagerly) { final Action0 unsubscribe = mock(Action0.class); Func0 resourceFactory = new Func0() { @Override @@ -185,9 +229,10 @@ public Observable call(Subscription subscription) { throw new TestException(); } }; - + try { - Observable.using(resourceFactory, observableFactory, disposeSubscription).toBlocking().last(); + Observable.using(resourceFactory, observableFactory, disposeSubscription).toBlocking() + .last(); fail("Should throw a TestException when the observableFactory throws it"); } catch (TestException e) { // Make sure that unsubscribe is called so that users can close @@ -198,6 +243,15 @@ public Observable call(Subscription subscription) { @Test public void testUsingWithObservableFactoryErrorInOnSubscribe() { + performTestUsingWithObservableFactoryErrorInOnSubscribe(false); + } + + @Test + public void testUsingWithObservableFactoryErrorInOnSubscribeDisposeEagerly() { + performTestUsingWithObservableFactoryErrorInOnSubscribe(true); + } + + private void performTestUsingWithObservableFactoryErrorInOnSubscribe(boolean disposeEagerly) { final Action0 unsubscribe = mock(Action0.class); Func0 resourceFactory = new Func0() { @Override @@ -217,11 +271,11 @@ public void call(Subscriber t1) { }); } }; - - try { - Observable.using(resourceFactory, observableFactory, disposeSubscription).toBlocking().last(); + Observable + .using(resourceFactory, observableFactory, disposeSubscription, disposeEagerly) + .toBlocking().last(); fail("Should throw a TestException when the observableFactory throws it"); } catch (TestException e) { // Make sure that unsubscribe is called so that users can close @@ -229,4 +283,153 @@ public void call(Subscriber t1) { verify(unsubscribe, times(1)).call(); } } + + @Test + public void testUsingDisposesEagerlyBeforeCompletion() { + final List events = new ArrayList(); + Func0 resourceFactory = createResourceFactory(events); + final Action0 completion = createOnCompletedAction(events); + final Action0 unsub =createUnsubAction(events); + + Func1> observableFactory = new Func1>() { + @Override + public Observable call(Resource resource) { + return Observable.from(resource.getTextFromWeb().split(" ")); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Observable observable = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), true).doOnUnsubscribe(unsub) + .doOnCompleted(completion); + observable.subscribe(observer); + + assertEquals(Arrays.asList("disposed", "completed", "unsub"), events); + + } + + @Test + public void testUsingDoesNotDisposesEagerlyBeforeCompletion() { + final List events = new ArrayList(); + Func0 resourceFactory = createResourceFactory(events); + final Action0 completion = createOnCompletedAction(events); + final Action0 unsub =createUnsubAction(events); + + Func1> observableFactory = new Func1>() { + @Override + public Observable call(Resource resource) { + return Observable.from(resource.getTextFromWeb().split(" ")); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Observable observable = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), false).doOnUnsubscribe(unsub) + .doOnCompleted(completion); + observable.subscribe(observer); + + assertEquals(Arrays.asList("completed", "unsub", "disposed"), events); + + } + + + + @Test + public void testUsingDisposesEagerlyBeforeError() { + 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 Observable call(Resource resource) { + return Observable.from(resource.getTextFromWeb().split(" ")).concatWith(Observable.error(new RuntimeException())); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Observable observable = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), true).doOnUnsubscribe(unsub) + .doOnError(onError); + observable.subscribe(observer); + + assertEquals(Arrays.asList("disposed", "error", "unsub"), events); + + } + + @Test + public void testUsingDoesNotDisposesEagerlyBeforeError() { + 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 Observable call(Resource resource) { + return Observable.from(resource.getTextFromWeb().split(" ")).concatWith(Observable.error(new RuntimeException())); + } + }; + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + Observable observable = Observable.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 Action0 createOnCompletedAction(final List events) { + return new Action0() { + @Override + public void call() { + events.add("completed"); + } + }; + } + } From a0c081806f51cf9f3d39ba10012e5157c18cd624 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Sat, 21 Feb 2015 10:26:25 -0800 Subject: [PATCH 172/857] 1.0.7 --- CHANGES.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index a7e2302728..0a661b49ef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,49 @@ # RxJava Releases # +### Version 1.0.7 – Feburary 21st 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.7%7C)) ### + +This release includes some bug fixes along with a new operator and performance enhancements. + +#### Experimental Operator + +Note that these APIs [may still change or be removed altogether](https://github.com/ReactiveX/RxJava#experimental) since they are marked as `@Experimental`. + +##### withLatestFrom(Observable, Selector) + +This allows combining all values from one `Observable` with the latest value from a second `Observable` at each `onNext`. + +For example: + +```java +Observable a = Observable.interval(1, TimeUnit.SECONDS); +Observable b = Observable.interval(250, TimeUnit.MILLISECONDS); + + +a.withLatestFrom(b, (x, y) -> new long[] { x, y }) + .toBlocking() + .forEach(pair -> System.out.println("a: " + pair[0] + " b: " + pair[1])); +``` + +This outputs: + +``` +a: 0 b: 3 +a: 1 b: 6 +a: 2 b: 11 +a: 3 b: 15 +a: 4 b: 19 +a: 5 b: 23 +a: 6 b: 27 +``` + + +#### Changes + +https://github.com/ReactiveX/RxJava/pull/2760 Operator: WithLatestFrom +https://github.com/ReactiveX/RxJava/pull/2762 Optimized isUnsubscribed check +https://github.com/ReactiveX/RxJava/pull/2759 Observable.using should use unsafeSubscribe and enable eager disposal +https://github.com/ReactiveX/RxJava/pull/2655 SwitchOnNext: fix upstream producer replacing the ops own producer + ### Version 1.0.6 – Feburary 11th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.6%7C)) ### This release adds an experimental operator and fixes several bugs. From dded0d2f3d7e4b715040f5a588cb2d709cad9493 Mon Sep 17 00:00:00 2001 From: David Gross Date: Sun, 22 Feb 2015 11:43:05 -0800 Subject: [PATCH 173/857] javadocs: withLatestFrom diagram, @since annotations, standardize on formatting and terminology, add compiler nags for missing content --- src/main/java/rx/Observable.java | 46 ++++++++++--------- .../rx/observers/SerializedSubscriber.java | 10 ++-- .../subscriptions/CompositeSubscription.java | 3 ++ 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 8bfadcb311..1faddbb819 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -2573,9 +2573,10 @@ public final static Observable using( /** * Constructs an Observable that creates a dependent resource object which is disposed of just before - * termination if disposeEagerly is set to 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. disposeAction will only be called once per subscription. + * 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 + * only be called once per subscription. *

* *

@@ -2583,6 +2584,7 @@ public final static Observable using( *
{@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 Observable * @param observableFactory @@ -2590,8 +2592,8 @@ public final static Observable using( * @param disposeAction * the function that will dispose of the resource * @param disposeEagerly - * if true then disposal will happen either on unsubscription or just before emission of - * a terminal event (onComplete or onError). + * 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 Observable 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. @@ -8805,24 +8807,24 @@ public final Observable unsubscribeOn(Scheduler scheduler) { } /** - * Merges the specified observable sequence into this Observable sequence by using the resultSelector - * function only when the source observable sequence (this instance) produces an element. - *
-     * ----A-------B------C----->  o1
-     *
-     * --0----1-2----3-4-------->  o2
-     *
-     *     |       |      |
-     *     V       V      V
+     * Merges the specified Observable into this Observable sequence by using the {@code resultSelector}
+     * function only when the source Observable (this instance) emits an item.
+     * 

+ * * - * (A,0) (B,2) (C,4) - *

- * @param other the other observable sequence - * @param resultSelector the function to call when this Observable emits an element and the other - * observable sequence has already emitted a value. - * @return an Observable that merges the specified observable sequence into this Observable sequence - * by using the resultSelector function only when the source observable sequence - * (this instance) produces an element + * @warn "Backpressure Support" section missing from javadoc + * @warn "Scheduler" section missing from javadoc + * @param other + * the other Observable + * @param resultSelector + * the function to call when this Observable emits an item and the other Observable has already + * emitted an item, to generate the item to be emitted by the resulting Observable + * @return an Observable that merges the specified Observable into this Observable by using the + * {@code resultSelector} function only when the source Observable sequence (this instance) emits an + * item + * @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) + * @see ReactiveX operators documentation: CombineLatest */ @Experimental public final Observable withLatestFrom(Observable other, Func2 resultSelector) { diff --git a/src/main/java/rx/observers/SerializedSubscriber.java b/src/main/java/rx/observers/SerializedSubscriber.java index e277506bcd..0b16549da3 100644 --- a/src/main/java/rx/observers/SerializedSubscriber.java +++ b/src/main/java/rx/observers/SerializedSubscriber.java @@ -39,12 +39,16 @@ public class SerializedSubscriber extends Subscriber { public SerializedSubscriber(Subscriber s) { this(s, true); } + /** * Constructor for wrapping and serializing a subscriber optionally sharing the same underlying subscription * list. - * @param s the subscriber to wrap and serialize - * @param shareSubscriptions if {@code true}, the same subscription list is shared between this - * subscriber and {@code s}. + * + * @param s + * the subscriber to wrap and serialize + * @param shareSubscriptions + * if {@code true}, the same subscription list is shared between this subscriber and {@code s}. + * @since 1.0.7 */ public SerializedSubscriber(Subscriber s, boolean shareSubscriptions) { super(s, shareSubscriptions); diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index b4d82ce092..d275d7ebfb 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -150,9 +150,12 @@ private static void unsubscribeFromAll(Collection subscriptions) { } Exceptions.throwIfAny(es); } + /** * Returns true if this composite is not unsubscribed and contains subscriptions. + * * @return {@code true} if this composite is not unsubscribed and contains subscriptions. + * @since 1.0.7 */ public boolean hasSubscriptions() { if (!unsubscribed) { From cea2d9df417ff9ee036a4031e4a17fe0c725282d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 23 Feb 2015 09:31:18 +0100 Subject: [PATCH 174/857] Optimized scalar observeOn/subscribeOn --- src/main/java/rx/Observable.java | 6 ++ .../schedulers/EventLoopsScheduler.java | 19 +++-- .../util/ScalarSynchronousObservable.java | 71 ++++++++++++++++++- src/main/java/rx/schedulers/Schedulers.java | 1 + 4 files changed, 90 insertions(+), 7 deletions(-) rename src/main/java/rx/{ => internal}/schedulers/EventLoopsScheduler.java (88%) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 1faddbb819..a09da42012 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5175,6 +5175,9 @@ public final Observable mergeWith(Observable t1) { * @see #subscribeOn */ public final Observable observeOn(Scheduler scheduler) { + if (this instanceof ScalarSynchronousObservable) { + return ((ScalarSynchronousObservable)this).scalarScheduleOn(scheduler); + } return lift(new OperatorObserveOn(scheduler)); } @@ -7597,6 +7600,9 @@ public final Subscription subscribe(Subscriber subscriber) { * @see #observeOn */ public final Observable subscribeOn(Scheduler scheduler) { + if (this instanceof ScalarSynchronousObservable) { + return ((ScalarSynchronousObservable)this).scalarScheduleOn(scheduler); + } return nest().lift(new OperatorSubscribeOn(scheduler)); } diff --git a/src/main/java/rx/schedulers/EventLoopsScheduler.java b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java similarity index 88% rename from src/main/java/rx/schedulers/EventLoopsScheduler.java rename to src/main/java/rx/internal/schedulers/EventLoopsScheduler.java index a674dfbc22..91a5440227 100644 --- a/src/main/java/rx/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java @@ -13,13 +13,11 @@ * 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.Subscription; import rx.functions.Action0; -import rx.internal.schedulers.NewThreadWorker; -import rx.internal.schedulers.ScheduledAction; import rx.internal.util.RxThreadFactory; import rx.subscriptions.CompositeSubscription; import rx.subscriptions.Subscriptions; @@ -27,7 +25,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -/* package */class EventLoopsScheduler extends Scheduler { +public class EventLoopsScheduler extends Scheduler { /** Manages a fixed number of workers. */ private static final String THREAD_NAME_PREFIX = "RxComputationThreadPool-"; private static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); @@ -76,7 +74,7 @@ public PoolWorker getEventLoop() { * Create a scheduler with pool size equal to the available processor * count and using least-recent worker selection policy. */ - EventLoopsScheduler() { + public EventLoopsScheduler() { pool = new FixedSchedulerPool(); } @@ -84,6 +82,17 @@ public PoolWorker getEventLoop() { public Worker createWorker() { return new EventLoopWorker(pool.getEventLoop()); } + + /** + * Schedules the action directly on one of the event loop workers + * without the additional infrastructure and checking. + * @param action the action to schedule + * @return the subscription + */ + public Subscription scheduleDirect(Action0 action) { + PoolWorker pw = pool.getEventLoop(); + return pw.scheduleActual(action, -1, TimeUnit.NANOSECONDS); + } private static class EventLoopWorker extends Scheduler.Worker { private final CompositeSubscription innerSubscription = new CompositeSubscription(); diff --git a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java index e145349b42..c350c895c4 100644 --- a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java +++ b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java @@ -15,8 +15,10 @@ */ package rx.internal.util; -import rx.Observable; -import rx.Subscriber; +import rx.*; +import rx.Scheduler.Worker; +import rx.functions.Action0; +import rx.internal.schedulers.EventLoopsScheduler; public final class ScalarSynchronousObservable extends Observable { @@ -49,5 +51,70 @@ public void call(Subscriber s) { public T get() { return t; } + /** + * Customized observeOn/subscribeOn implementation which emits the scalar + * value directly or with less overhead on the specified scheduler. + * @param scheduler the target scheduler + * @return the new observable + */ + public Observable scalarScheduleOn(Scheduler scheduler) { + if (scheduler instanceof EventLoopsScheduler) { + EventLoopsScheduler es = (EventLoopsScheduler) scheduler; + return create(new DirectScheduledEmission(es, t)); + } + return create(new NormalScheduledEmission(scheduler, t)); + } + + /** Optimized observeOn for scalar value observed on the EventLoopsScheduler. */ + static final class DirectScheduledEmission implements OnSubscribe { + private final EventLoopsScheduler es; + private final T value; + DirectScheduledEmission(EventLoopsScheduler es, T value) { + this.es = es; + this.value = value; + } + @Override + public void call(final Subscriber child) { + child.add(es.scheduleDirect(new ScalarSynchronousAction(child, value))); + } + } + /** Emits a scalar value on a general scheduler. */ + static final class NormalScheduledEmission implements OnSubscribe { + private final Scheduler scheduler; + private final T value; + + NormalScheduledEmission(Scheduler scheduler, T value) { + this.scheduler = scheduler; + this.value = value; + } + + @Override + public void call(final Subscriber subscriber) { + Worker worker = scheduler.createWorker(); + subscriber.add(worker); + worker.schedule(new ScalarSynchronousAction(subscriber, value)); + } + } + /** Action that emits a single value when called. */ + static final class ScalarSynchronousAction implements Action0 { + private final Subscriber subscriber; + private final T value; + private ScalarSynchronousAction(Subscriber subscriber, + T value) { + this.subscriber = subscriber; + this.value = value; + } + + @Override + public void call() { + try { + subscriber.onNext(value); + } catch (Throwable t) { + subscriber.onError(t); + return; + } + subscriber.onCompleted(); + } + } } diff --git a/src/main/java/rx/schedulers/Schedulers.java b/src/main/java/rx/schedulers/Schedulers.java index 374448d695..8ded001e0e 100644 --- a/src/main/java/rx/schedulers/Schedulers.java +++ b/src/main/java/rx/schedulers/Schedulers.java @@ -16,6 +16,7 @@ package rx.schedulers; import rx.Scheduler; +import rx.internal.schedulers.EventLoopsScheduler; import rx.plugins.RxJavaPlugins; import java.util.concurrent.Executor; From 14ed8d40c7789d560b6f2a2b044defb56e9d23cd Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 23 Feb 2015 09:49:00 -0800 Subject: [PATCH 175/857] Fix typo --- src/main/java/rx/observables/AbstractOnSubscribe.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/observables/AbstractOnSubscribe.java b/src/main/java/rx/observables/AbstractOnSubscribe.java index 6dbe7ad44a..ea78c56c75 100644 --- a/src/main/java/rx/observables/AbstractOnSubscribe.java +++ b/src/main/java/rx/observables/AbstractOnSubscribe.java @@ -84,7 +84,7 @@ * instance into an {@code Observable} fluently. * *

Examples

- * Note: these examples use the lambda-helper factories to avoid boilerplane. + * Note: these examples use the lambda-helper factories to avoid boilerplate. * *

Implement: just

*


From 20430945276d8522dc3fe3e16463330b1a311c1c Mon Sep 17 00:00:00 2001
From: Dave Moten 
Date: Tue, 24 Feb 2015 09:06:43 +1100
Subject: [PATCH 176/857] add request overflow check for combineLatest

---
 .../operators/OnSubscribeCombineLatest.java   |  2 +-
 .../OnSubscribeCombineLatestTest.java         | 35 +++++++++++++++++++
 2 files changed, 36 insertions(+), 1 deletion(-)

diff --git a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java
index c5ea0d1291..1537f91aaf 100644
--- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java
+++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java
@@ -110,7 +110,7 @@ public MultiSourceProducer(final Subscriber child, final List> sources = Arrays.asList(Observable.from(Arrays.asList(1,2,3,4)), Observable.from(Arrays.asList(5,6,7,8)));
+        Observable o = Observable.combineLatest(sources,new FuncN() {
+            @Override
+            public Integer call(Object... args) {
+               return (Integer) args[0];
+            }});
+        //should get at least 4
+        final CountDownLatch latch = new CountDownLatch(4);
+        o.subscribeOn(Schedulers.computation()).subscribe(new Subscriber() {
+            
+            @Override
+            public void onStart() {
+                request(2);
+            }
+
+            @Override
+            public void onCompleted() {
+                //ignore
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                throw new RuntimeException(e);
+            }
+
+            @Override
+            public void onNext(Integer t) {
+                latch.countDown();
+                request(Long.MAX_VALUE-1);
+            }});
+        assertTrue(latch.await(10, TimeUnit.SECONDS));
+    }
 
 }

From 9edfdacd6418be902491d4e727e97067438a6e42 Mon Sep 17 00:00:00 2001
From: Dave Moten 
Date: Tue, 24 Feb 2015 11:20:48 +1100
Subject: [PATCH 177/857] add request overflow check to
 OperatorOnBackpressureDrop

---
 .../operators/OperatorOnBackpressureDrop.java |  2 +-
 .../OperatorOnBackpressureDropTest.java       | 52 ++++++++++++++++++-
 2 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java
index ef92ccd542..5e81162821 100644
--- a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java
+++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java
@@ -43,7 +43,7 @@ public Subscriber call(final Subscriber child) {
 
             @Override
             public void request(long n) {
-                requested.getAndAdd(n);
+                BackpressureUtils.getAndAddRequest(requested, n);
             }
 
         });
diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java
index a4e28780f1..66b88f4ecf 100644
--- a/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java
+++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java
@@ -17,6 +17,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+
 import org.junit.Test;
 
 import rx.Observable;
@@ -27,8 +30,6 @@
 import rx.observers.TestSubscriber;
 import rx.schedulers.Schedulers;
 
-import java.util.concurrent.CountDownLatch;
-
 public class OperatorOnBackpressureDropTest {
 
     @Test
@@ -87,6 +88,35 @@ public void onNext(Long t) {
         ts.assertNoErrors();
         assertEquals(0, ts.getOnNextEvents().get(0).intValue());
     }
+    
+    @Test
+    public void testRequestOverflow() throws InterruptedException {
+        final AtomicInteger count = new AtomicInteger();
+        int n = 10;
+        range(n).onBackpressureDrop().subscribe(new Subscriber() {
+
+            @Override
+            public void onStart() {
+                request(10);
+            }
+            
+            @Override
+            public void onCompleted() {
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                throw new RuntimeException(e);
+            }
+
+            @Override
+            public void onNext(Long t) {
+                count.incrementAndGet();
+                //cause overflow of requested if not handled properly in onBackpressureDrop operator
+                request(Long.MAX_VALUE-1);
+            }});
+        assertEquals(n, count.get());
+    }
 
     static final Observable infinite = Observable.create(new OnSubscribe() {
 
@@ -99,4 +129,22 @@ public void call(Subscriber s) {
         }
 
     });
+    
+    private static final Observable range(final long n) {
+        return Observable.create(new OnSubscribe() {
+
+            @Override
+            public void call(Subscriber s) {
+                for (long i=0;i < n;i++) {
+                    if (s.isUnsubscribed()) {
+                        break;
+                    }
+                    s.onNext(i);
+                }
+                s.onCompleted();
+            }
+    
+        });
+    }
+    
 }

From 608dd0f5f98dbf57bb0da031020888e94b2d013b Mon Sep 17 00:00:00 2001
From: Dave Moten 
Date: Tue, 24 Feb 2015 21:56:52 +1100
Subject: [PATCH 178/857] add request overflow check to OnSubscribeRange

---
 .../internal/operators/OnSubscribeRange.java  |  2 +-
 .../operators/OnSubscribeRangeTest.java       | 30 +++++++++++++++++++
 2 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/src/main/java/rx/internal/operators/OnSubscribeRange.java b/src/main/java/rx/internal/operators/OnSubscribeRange.java
index f7a95bd216..f648e6f414 100644
--- a/src/main/java/rx/internal/operators/OnSubscribeRange.java
+++ b/src/main/java/rx/internal/operators/OnSubscribeRange.java
@@ -73,7 +73,7 @@ public void request(long n) {
                 }
             } else if (n > 0) {
                 // backpressure is requested
-                long _c = REQUESTED_UPDATER.getAndAdd(this, n);
+                long _c = BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER,this, n);
                 if (_c == 0) {
                     while (true) {
                         /*
diff --git a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java
index 21dc5599e2..9b06cdb4d0 100644
--- a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java
+++ b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java
@@ -32,6 +32,7 @@
 
 import rx.Observable;
 import rx.Observer;
+import rx.Subscriber;
 import rx.functions.Action1;
 import rx.internal.util.RxRingBuffer;
 import rx.observers.TestSubscriber;
@@ -190,4 +191,33 @@ public void testWithBackpressureRequestWayMore() {
         ts.assertReceivedOnNext(list);
         ts.assertTerminalEvent();
     }
+    
+    @Test
+    public void testRequestOverflow() {
+        final AtomicInteger count = new AtomicInteger();
+        int n = 10;
+        Observable.range(1, n).subscribe(new Subscriber() {
+
+            @Override
+            public void onStart() {
+                request(2);
+            }
+            
+            @Override
+            public void onCompleted() {
+                //do nothing
+            }
+
+            @Override
+            public void onError(Throwable e) {
+                throw new RuntimeException(e);
+            }
+
+            @Override
+            public void onNext(Integer t) {
+                count.incrementAndGet();
+                request(Long.MAX_VALUE - 1);
+            }});
+        assertEquals(n, count.get());
+    }
 }

From d747d5259280ad1c7a17415e51082c614d1b2ab7 Mon Sep 17 00:00:00 2001
From: Steven Wu 
Date: Thu, 26 Feb 2015 10:23:03 -0800
Subject: [PATCH 179/857] fix several typos

---
 .../rx/internal/operators/OperatorOnBackpressureBufferTest.java | 2 +-
 .../rx/internal/operators/OperatorOnBackpressureDropTest.java   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java
index ff2d8e0c6f..004764dd0b 100644
--- a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java
+++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java
@@ -93,7 +93,7 @@ public void testFixBackpressureBufferNegativeCapacity() throws InterruptedExcept
 
     @Test(expected = IllegalArgumentException.class)
     public void testFixBackpressureBufferZeroCapacity() throws InterruptedException {
-        Observable.empty().onBackpressureBuffer(-1);
+        Observable.empty().onBackpressureBuffer(0);
     }
 
     @Test
diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java
index a4e28780f1..289cf81784 100644
--- a/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java
+++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java
@@ -46,7 +46,7 @@ public void testNoBackpressureSupport() {
     @Test(timeout = 500)
     public void testWithObserveOn() throws InterruptedException {
         TestSubscriber ts = new TestSubscriber();
-        Observable.range(0, RxRingBuffer.SIZE * 10).onBackpressureDrop().onBackpressureDrop().observeOn(Schedulers.io()).subscribe(ts);
+        Observable.range(0, RxRingBuffer.SIZE * 10).onBackpressureDrop().observeOn(Schedulers.io()).subscribe(ts);
         ts.awaitTerminalEvent();
     }
 

From 382d6dfa95e9f55c48d99883f9db460e9c16da18 Mon Sep 17 00:00:00 2001
From: Dave Moten 
Date: Sat, 28 Feb 2015 23:03:22 +1100
Subject: [PATCH 180/857] OperatorMulticast.connect(connection) should return
 first subscription on multiple calls, address possible race condition
 provoking IAE

---
 .../internal/operators/OperatorMulticast.java | 54 ++++++++++---------
 ...stTest.java => OperatorMulticastTest.java} | 12 +++--
 2 files changed, 38 insertions(+), 28 deletions(-)
 rename src/test/java/rx/internal/operators/{OnSubscribeMulticastTest.java => OperatorMulticastTest.java} (93%)

diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java
index e294bfa8f4..8a15dd30ef 100644
--- a/src/main/java/rx/internal/operators/OperatorMulticast.java
+++ b/src/main/java/rx/internal/operators/OperatorMulticast.java
@@ -38,14 +38,16 @@
  *            the result value type
  */
 public final class OperatorMulticast extends ConnectableObservable {
-    final Observable source;
-    final Object guard;
-    final Func0> subjectFactory;
+    private final Observable source;
+    private final Object guard;
+    private final Func0> subjectFactory;
     private final AtomicReference> connectedSubject;
     private final List> waitingForConnect;
 
     /** Guarded by guard. */
-    Subscriber subscription;
+    private Subscriber subscription;
+    // wraps subscription above with for unsubscription using guard
+    private Subscription guardedSubscription;
 
     public OperatorMulticast(Observable source, final Func0> subjectFactory) {
         this(new Object(), new AtomicReference>(), new ArrayList>(), source, subjectFactory);
@@ -82,7 +84,8 @@ public void connect(Action1 connection) {
         // subscription is the state of whether we are connected or not
         synchronized (guard) {
             if (subscription != null) {
-                // already connected, return as there is nothing to do
+                // already connected
+                connection.call(guardedSubscription);
                 return;
             } else {
                 shouldSubscribe = true;
@@ -106,6 +109,21 @@ public void onNext(T args) {
                         subject.onNext(args);
                     }
                 };
+                guardedSubscription = Subscriptions.create(new Action0() {
+                    @Override
+                    public void call() {
+                        Subscription s;
+                        synchronized (guard) {
+                            s = subscription;
+                            subscription = null;
+                            guardedSubscription = null;
+                            connectedSubject.set(null);
+                        }
+                        if (s != null) {
+                            s.unsubscribe();
+                        }
+                    }
+                });
                 
                 // register any subscribers that are waiting with this new subject
                 for(Subscriber s : waitingForConnect) {
@@ -116,34 +134,22 @@ public void onNext(T args) {
                 // record the Subject so OnSubscribe can see it
                 connectedSubject.set(subject);
             }
+            
         }
 
         // in the lock above we determined we should subscribe, do it now outside the lock
         if (shouldSubscribe) {
             // register a subscription that will shut this down
-            connection.call(Subscriptions.create(new Action0() {
-                @Override
-                public void call() {
-                    Subscription s;
-                    synchronized (guard) {
-                        s = subscription;
-                        subscription = null;
-                        connectedSubject.set(null);
-                    }
-                    if (s != null) {
-                        s.unsubscribe();
-                    }
-                }
-            }));
+            connection.call(guardedSubscription);
 
             // now that everything is hooked up let's subscribe
             // as long as the subscription is not null (which can happen if already unsubscribed)
-            boolean subscriptionIsNull;
-            synchronized(guard) {
-                subscriptionIsNull = subscription == null;
+            Subscriber sub; 
+            synchronized (guard) {
+                sub = subscription;
             }
-            if (!subscriptionIsNull)
-                source.subscribe(subscription);
+            if (sub != null)
+                source.subscribe(sub);
         }
     }
 }
\ No newline at end of file
diff --git a/src/test/java/rx/internal/operators/OnSubscribeMulticastTest.java b/src/test/java/rx/internal/operators/OperatorMulticastTest.java
similarity index 93%
rename from src/test/java/rx/internal/operators/OnSubscribeMulticastTest.java
rename to src/test/java/rx/internal/operators/OperatorMulticastTest.java
index f5c506c8b2..5b3c57e4f6 100644
--- a/src/test/java/rx/internal/operators/OnSubscribeMulticastTest.java
+++ b/src/test/java/rx/internal/operators/OperatorMulticastTest.java
@@ -15,11 +15,13 @@
  */
 package rx.internal.operators;
 
+import static org.junit.Assert.assertEquals;
 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 org.junit.Assert;
 import org.junit.Test;
 
 import rx.Observer;
@@ -29,7 +31,7 @@
 import rx.subjects.PublishSubject;
 import rx.subjects.Subject;
 
-public class OnSubscribeMulticastTest {
+public class OperatorMulticastTest {
 
     @Test
     public void testMulticast() {
@@ -70,15 +72,17 @@ public void testMulticastConnectTwice() {
 
         source.onNext("one");
 
-        multicasted.connect();
-        multicasted.connect();
-
+        Subscription sub = multicasted.connect();
+        Subscription sub2 = multicasted.connect();
+        
         source.onNext("two");
         source.onCompleted();
 
         verify(observer, never()).onNext("one");
         verify(observer, times(1)).onNext("two");
         verify(observer, times(1)).onCompleted();
+        
+        assertEquals(sub, sub2);
 
     }
 

From 3b6707a41c88993ef3913987501dc79319fb171a Mon Sep 17 00:00:00 2001
From: Dave Moten 
Date: Sat, 28 Feb 2015 23:05:21 +1100
Subject: [PATCH 181/857] fix typo in comment

---
 src/main/java/rx/internal/operators/OperatorMulticast.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java
index 8a15dd30ef..d6e379f4ce 100644
--- a/src/main/java/rx/internal/operators/OperatorMulticast.java
+++ b/src/main/java/rx/internal/operators/OperatorMulticast.java
@@ -46,7 +46,7 @@ public final class OperatorMulticast extends ConnectableObservable {
 
     /** Guarded by guard. */
     private Subscriber subscription;
-    // wraps subscription above with for unsubscription using guard
+    // wraps subscription above for unsubscription using guard
     private Subscription guardedSubscription;
 
     public OperatorMulticast(Observable source, final Func0> subjectFactory) {

From a85a11eb872d04cfe7eb4af799eb1a9ca0d10511 Mon Sep 17 00:00:00 2001
From: Dave Moten 
Date: Sun, 1 Mar 2015 10:46:20 +1100
Subject: [PATCH 182/857] revert visibility of fields and add == this check for
 guarded subscription

---
 .../internal/operators/OperatorMulticast.java | 27 +++++++++++--------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java
index d6e379f4ce..78a93f4dd3 100644
--- a/src/main/java/rx/internal/operators/OperatorMulticast.java
+++ b/src/main/java/rx/internal/operators/OperatorMulticast.java
@@ -38,11 +38,11 @@
  *            the result value type
  */
 public final class OperatorMulticast extends ConnectableObservable {
-    private final Observable source;
-    private final Object guard;
-    private final Func0> subjectFactory;
-    private final AtomicReference> connectedSubject;
-    private final List> waitingForConnect;
+    final Observable source;
+    final Object guard;
+    final Func0> subjectFactory;
+    final AtomicReference> connectedSubject;
+    final List> waitingForConnect;
 
     /** Guarded by guard. */
     private Subscriber subscription;
@@ -109,21 +109,26 @@ public void onNext(T args) {
                         subject.onNext(args);
                     }
                 };
-                guardedSubscription = Subscriptions.create(new Action0() {
+                final AtomicReference gs = new AtomicReference();
+                gs.set(Subscriptions.create(new Action0() {
                     @Override
                     public void call() {
                         Subscription s;
                         synchronized (guard) {
-                            s = subscription;
-                            subscription = null;
-                            guardedSubscription = null;
-                            connectedSubject.set(null);
+                            if ( guardedSubscription == gs.get()) {
+                                s = subscription;
+                                subscription = null;
+                                guardedSubscription = null;
+                                connectedSubject.set(null);
+                            } else 
+                                return;
                         }
                         if (s != null) {
                             s.unsubscribe();
                         }
                     }
-                });
+                }));
+                guardedSubscription = gs.get();
                 
                 // register any subscribers that are waiting with this new subject
                 for(Subscriber s : waitingForConnect) {

From 290602edd6bee476afef7c0a03ce48d3d893533c Mon Sep 17 00:00:00 2001
From: Steven Wu 
Date: Thu, 26 Feb 2015 10:09:33 -0800
Subject: [PATCH 183/857] issue-2764: add new operator
 onBackpressureDrop(Action1 onDrop)

---
 src/main/java/rx/Observable.java              | 21 +++++
 .../operators/OperatorOnBackpressureDrop.java | 20 ++++-
 src/test/java/rx/BackpressureTests.java       | 83 +++++++++++++++++--
 3 files changed, 118 insertions(+), 6 deletions(-)

diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java
index 8bfadcb311..0bb9f0383e 100644
--- a/src/main/java/rx/Observable.java
+++ b/src/main/java/rx/Observable.java
@@ -5258,6 +5258,27 @@ 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 discard,
+     * rather than emit, those items that its observer is not prepared to observe.
+     * 

+ * + *

+ * If the downstream request count hits 0 then the Observable will refrain from calling {@code onNext} until + * the observer invokes {@code request(n)} again to increase the request count. + *

+ *
Scheduler:
+ *
{@code onBackpressureDrop} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param onDrop the action to invoke for each item dropped. onDrop action should be fast and should never block. + * @return the source Observable modified to drop {@code onNext} notifications on overflow + * @see ReactiveX operators documentation: backpressure operators + */ + public final Observable onBackpressureDrop(Action1 onDrop) { + return lift(new OperatorOnBackpressureDrop(onDrop)); + } + /** * 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/OperatorOnBackpressureDrop.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java index ef92ccd542..80dc5c0711 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java @@ -20,13 +20,16 @@ import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; +import rx.functions.Action1; public class OperatorOnBackpressureDrop implements Operator { + /** Lazy initialization via inner-class holder. */ private static final class Holder { /** A singleton instance. */ static final OperatorOnBackpressureDrop INSTANCE = new OperatorOnBackpressureDrop(); } + /** * @return a singleton instance of this stateless operator. */ @@ -34,7 +37,17 @@ private static final class Holder { public static OperatorOnBackpressureDrop instance() { return (OperatorOnBackpressureDrop)Holder.INSTANCE; } - private OperatorOnBackpressureDrop() { } + + private final Action1 onDrop; + + private OperatorOnBackpressureDrop() { + this(null); + } + + public OperatorOnBackpressureDrop(Action1 onDrop) { + this.onDrop = onDrop; + } + @Override public Subscriber call(final Subscriber child) { final AtomicLong requested = new AtomicLong(); @@ -68,6 +81,11 @@ public void onNext(T t) { if (requested.get() > 0) { child.onNext(t); requested.decrementAndGet(); + } else { + // item dropped + if(onDrop != null) { + onDrop.call(t); + } } } diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index f54b8b67d5..799564d50d 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -23,6 +23,7 @@ import org.junit.*; +import org.junit.rules.TestName; import rx.Observable.OnSubscribe; import rx.exceptions.MissingBackpressureException; import rx.functions.*; @@ -33,6 +34,9 @@ public class BackpressureTests { + @Rule + public TestName testName = new TestName(); + @After public void doAfterTest() { TestObstructionDetection.checkObstruction(); @@ -424,18 +428,56 @@ public void testOnBackpressureDrop() { .map(SLOW_PASS_THRU).take(NUM).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); - - + List onNextEvents = ts.getOnNextEvents(); assertEquals(NUM, onNextEvents.size()); Integer lastEvent = onNextEvents.get(NUM - 1); - + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent); // it drop, so we should get some number far higher than what would have sequentially incremented assertTrue(NUM - 1 <= lastEvent.intValue()); } } + + @Test(timeout = 10000) + public void testOnBackpressureDropWithAction() { + for (int i = 0; i < 100; i++) { + final AtomicInteger emitCount = new AtomicInteger(); + final AtomicInteger dropCount = new AtomicInteger(); + final AtomicInteger passCount = new AtomicInteger(); + final int NUM = (int) (RxRingBuffer.SIZE * 1.5); // > 1 so that take doesn't prevent buffer overflow + TestSubscriber ts = new TestSubscriber(); + firehose(emitCount).onBackpressureDrop(new Action1() { + @Override + public void call(Integer i) { + dropCount.incrementAndGet(); + } + }) + .doOnNext(new Action1() { + @Override + public void call(Integer integer) { + passCount.incrementAndGet(); + } + }) + .observeOn(Schedulers.computation()) + .map(SLOW_PASS_THRU).take(NUM).subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + + List onNextEvents = ts.getOnNextEvents(); + Integer lastEvent = onNextEvents.get(NUM - 1); + System.out.println(testName.getMethodName() + " => Received: " + onNextEvents.size() + " Passed: " + passCount.get() + " Dropped: " + dropCount.get() + " Emitted: " + emitCount.get() + " Last value: " + lastEvent); + assertEquals(NUM, onNextEvents.size()); + // in reality, NUM < passCount + assertTrue(NUM <= passCount.get()); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(NUM - 1 <= lastEvent.intValue()); + assertTrue(0 < dropCount.get()); + assertEquals(emitCount.get(), passCount.get() + dropCount.get()); + } + } + @Test(timeout = 10000) public void testOnBackpressureDropSynchronous() { for (int i = 0; i < 100; i++) { @@ -446,18 +488,49 @@ public void testOnBackpressureDropSynchronous() { .map(SLOW_PASS_THRU).take(NUM).subscribe(ts); ts.awaitTerminalEvent(); ts.assertNoErrors(); - + List onNextEvents = ts.getOnNextEvents(); assertEquals(NUM, onNextEvents.size()); Integer lastEvent = onNextEvents.get(NUM - 1); - + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent); // it drop, so we should get some number far higher than what would have sequentially incremented assertTrue(NUM - 1 <= lastEvent.intValue()); } } + @Test(timeout = 10000) + public void testOnBackpressureDropSynchronousWithAction() { + for (int i = 0; i < 100; i++) { + final AtomicInteger dropCount = new AtomicInteger(); + int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow + AtomicInteger c = new AtomicInteger(); + TestSubscriber ts = new TestSubscriber(); + firehose(c).onBackpressureDrop(new Action1() { + @Override + public void call(Integer i) { + dropCount.incrementAndGet(); + } + }) + .map(SLOW_PASS_THRU).take(NUM).subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + + List onNextEvents = ts.getOnNextEvents(); + assertEquals(NUM, onNextEvents.size()); + + Integer lastEvent = onNextEvents.get(NUM - 1); + + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Dropped: " + dropCount.get() + " Emitted: " + c.get() + " Last value: " + lastEvent); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(NUM - 1 <= lastEvent.intValue()); + // no drop in synchronous mode + assertEquals(0, dropCount.get()); + assertEquals(c.get(), onNextEvents.size()); + } + } + @Test(timeout = 2000) public void testOnBackpressureBuffer() { int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow From 379f07d902bffa0de2a2d5578ca6a88a2caf3d0e Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sun, 1 Mar 2015 13:56:26 +1100 Subject: [PATCH 184/857] shouldSubscribe boolean variable not required --- .../internal/operators/OperatorMulticast.java | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java index 78a93f4dd3..4d5d10f4f3 100644 --- a/src/main/java/rx/internal/operators/OperatorMulticast.java +++ b/src/main/java/rx/internal/operators/OperatorMulticast.java @@ -79,8 +79,6 @@ public void call(Subscriber subscriber) { public void connect(Action1 connection) { // each time we connect we create a new Subject and Subscription - boolean shouldSubscribe = false; - // subscription is the state of whether we are connected or not synchronized (guard) { if (subscription != null) { @@ -88,7 +86,6 @@ public void connect(Action1 connection) { connection.call(guardedSubscription); return; } else { - shouldSubscribe = true; // we aren't connected, so let's create a new Subject and connect final Subject subject = subjectFactory.call(); // create new Subscriber that will pass-thru to the subject we just created @@ -143,18 +140,16 @@ public void call() { } // in the lock above we determined we should subscribe, do it now outside the lock - if (shouldSubscribe) { - // register a subscription that will shut this down - connection.call(guardedSubscription); + // register a subscription that will shut this down + connection.call(guardedSubscription); - // now that everything is hooked up let's subscribe - // as long as the subscription is not null (which can happen if already unsubscribed) - Subscriber sub; - synchronized (guard) { - sub = subscription; - } - if (sub != null) - source.subscribe(sub); + // now that everything is hooked up let's subscribe + // as long as the subscription is not null (which can happen if already unsubscribed) + Subscriber sub; + synchronized (guard) { + sub = subscription; } + if (sub != null) + source.subscribe(sub); } } \ No newline at end of file From 0fd9c762aa2deed5d09afd6dff41bdd7090c3283 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 2 Mar 2015 14:52:32 +0800 Subject: [PATCH 185/857] Fix the bug that 'publish' will cache items when no subscriber --- .../internal/operators/OperatorPublish.java | 28 +++++++++++++++---- .../operators/OperatorPublishTest.java | 17 +++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index de81e93241..193bdba6c4 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -213,6 +213,10 @@ public synchronized boolean canEmitWithDecrement() { return false; } + public synchronized boolean hasNoSubscriber() { + return subscribers.length == 0; + } + public synchronized void incrementOutstandingAfterFailedEmit() { outstandingRequests++; } @@ -308,11 +312,13 @@ public void emit(Object t) throws MissingBackpressureException { } private void requestMoreAfterEmission(int emitted) { - OriginSubscriber origin = state.getOrigin(); - if (emitted > 0 && origin != null) { - long r = origin.originOutstanding.addAndGet(-emitted); - if (r <= origin.THRESHOLD) { - origin.requestMore(RxRingBuffer.SIZE - origin.THRESHOLD); + if (emitted > 0) { + OriginSubscriber origin = state.getOrigin(); + if (origin != null) { + long r = origin.originOutstanding.addAndGet(-emitted); + if (r <= origin.THRESHOLD) { + origin.requestMore(RxRingBuffer.SIZE - origin.THRESHOLD); + } } } } @@ -336,8 +342,18 @@ public void drainQueue(OriginSubscriber originSubscriber) { * If we want to batch this then we need to account for new subscribers arriving with a lower request count * concurrently while iterating the batch ... or accept that they won't */ - while (true) { + if (localState.hasNoSubscriber()) { + // Drop items due to no subscriber + if (localBuffer.poll() == null) { + // Exit due to no more item + break; + } else { + // Keep dropping cached items. + continue; + } + } + boolean shouldEmit = localState.canEmitWithDecrement(); if (!shouldEmit) { break; diff --git a/src/test/java/rx/internal/operators/OperatorPublishTest.java b/src/test/java/rx/internal/operators/OperatorPublishTest.java index 611b1216b1..e8dc5312fb 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishTest.java @@ -30,6 +30,7 @@ import rx.observables.ConnectableObservable; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.schedulers.TestScheduler; public class OperatorPublishTest { @@ -242,4 +243,20 @@ public void call() { assertEquals(8, sourceEmission.get()); } + + @Test + public void testConnectWithNoSubscriber() { + TestScheduler scheduler = new TestScheduler(); + ConnectableObservable co = Observable.timer(10, 10, TimeUnit.MILLISECONDS, scheduler).take(3).publish(); + co.connect(); + // Emit 0 + scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); + TestSubscriber subscriber = new TestSubscriber(); + co.subscribe(subscriber); + // Emit 1 and 2 + scheduler.advanceTimeBy(50, TimeUnit.MILLISECONDS); + subscriber.assertReceivedOnNext(Arrays.asList(1L, 2L)); + subscriber.assertNoErrors(); + subscriber.assertTerminalEvent(); + } } From 8758fdd98cca2ed065654bb5a93a0351327aeda0 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 4 Mar 2015 13:57:09 -0800 Subject: [PATCH 186/857] Add Experimental to onBackpressureDrop(Action1) --- src/main/java/rx/Observable.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 01e14e0e12..b65fd94f5b 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5279,7 +5279,10 @@ public final Observable onBackpressureBuffer(long capacity, Action0 onOverflo * @param onDrop the action to invoke for each item dropped. onDrop action should be fast and should never block. * @return the source Observable modified to drop {@code onNext} notifications on overflow * @see ReactiveX operators documentation: backpressure operators + * @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 onBackpressureDrop(Action1 onDrop) { return lift(new OperatorOnBackpressureDrop(onDrop)); } From a57d4cd2d57d211408f12f9538afba7a390ab332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 6 Feb 2015 23:02:58 +0100 Subject: [PATCH 187/857] Implement hook to render specific types in OnNextValue --- .../java/rx/exceptions/OnErrorThrowable.java | 20 +++- .../java/rx/plugins/RxJavaErrorHandler.java | 44 +++++++++ .../java/rx/plugins/RxJavaPluginsTest.java | 96 ++++++++++++++++++- 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java index 3a38e74401..4e0969a6eb 100644 --- a/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -15,6 +15,9 @@ */ package rx.exceptions; +import rx.plugins.RxJavaErrorHandler; +import rx.plugins.RxJavaPlugins; + /** * Represents a {@code Throwable} that an {@code Observable} might notify its subscribers of, but that then can * be handled by an operator that is designed to recover from or react appropriately to such an error. You can @@ -131,11 +134,18 @@ public Object getValue() { /** * Render the object if it is a basic type. This avoids the library making potentially expensive - * or calls to toString() which may throw exceptions. See PR #1401 for details. + * or calls to toString() which may throw exceptions. + * + * If a specific behavior has been defined in the {@link RxJavaErrorHandler} plugin, some types + * may also have a specific rendering. Non-primitive types not managed by the plugin are rendered + * as the classname of the object. + *

+ * See PR #1401 and Issue #2468 for details. * * @param value * the item that the Observable was trying to emit at the time of the exception - * @return a string version of the object if primitive, otherwise the classname of the object + * @return a string version of the object if primitive or managed through error plugin, + * otherwise the classname of the object */ private static String renderValue(Object value){ if (value == null) { @@ -150,6 +160,12 @@ private static String renderValue(Object value){ if (value instanceof Enum) { return ((Enum) value).name(); } + + String pluggedRendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(value); + if (pluggedRendering != null) { + return pluggedRendering; + } + return value.getClass().getName() + ".class"; } } diff --git a/src/main/java/rx/plugins/RxJavaErrorHandler.java b/src/main/java/rx/plugins/RxJavaErrorHandler.java index b14b03f488..9dd52fdc3d 100644 --- a/src/main/java/rx/plugins/RxJavaErrorHandler.java +++ b/src/main/java/rx/plugins/RxJavaErrorHandler.java @@ -17,6 +17,7 @@ import rx.Observable; import rx.Subscriber; +import rx.exceptions.OnErrorThrowable; /** * Abstract class for defining error handling logic in addition to the normal @@ -25,6 +26,8 @@ * For example, all {@code Exception}s can be logged using this handler even if * {@link Subscriber#onError(Throwable)} is ignored or not provided when an {@link Observable} is subscribed to. *

+ * This plugin is also responsible for augmenting rendering of {@link OnErrorThrowable.OnNextValue}. + *

* See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: https://github.com/ReactiveX/RxJava/wiki/Plugins. */ @@ -44,4 +47,45 @@ public void handleError(Throwable e) { // do nothing by default } + protected static final String ERROR_IN_RENDERING_SUFFIX = ".errorRendering"; + + /** + * Receives items causing {@link OnErrorThrowable.OnNextValue} and gives a chance to choose the String + * representation of the item in the OnNextValue stacktrace rendering. Returns null if this type of item + * is not managed and should use default rendering. + *

+ * Note that primitive types are always rendered as their toString() value. + *

+ * If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by + * {@value #ERROR_IN_RENDERING_SUFFIX}. + * + * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. + * @return a short {@link String} representation of the item if one is known for its type, or null for default. + */ + public final String handleOnNextValueRendering(Object item) { + try { + return render(item); + } catch (Throwable t) { + return item.getClass().getName() + ERROR_IN_RENDERING_SUFFIX; + } + } + + /** + * Override this method to provide rendering for specific types other than primitive types and null. + *

+ * For performance and overhead reasons, this should should limit to a safe production of a short {@code String} + * (as large renderings will bloat up the stacktrace). Prefer to try/catch({@code Throwable}) all code + * inside this method implementation. + *

+ * If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by + * {@value #ERROR_IN_RENDERING_SUFFIX}. + * + * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. + * @return a short {@link String} representation of the item if one is known for its type, or null for default. + */ + protected String render (Object item) { + //do nothing by default + return null; + } + } diff --git a/src/test/java/rx/plugins/RxJavaPluginsTest.java b/src/test/java/rx/plugins/RxJavaPluginsTest.java index 12b5b12665..3d18923915 100644 --- a/src/test/java/rx/plugins/RxJavaPluginsTest.java +++ b/src/test/java/rx/plugins/RxJavaPluginsTest.java @@ -16,16 +16,25 @@ package rx.plugins; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; + import org.junit.After; import org.junit.Before; import org.junit.Test; import rx.Observable; import rx.Subscriber; +import rx.exceptions.OnErrorThrowable; +import rx.functions.Func1; public class RxJavaPluginsTest { @@ -78,7 +87,18 @@ public void handleError(Throwable e) { this.e = e; count++; } + } + public static class RxJavaErrorHandlerTestImplWithRender extends RxJavaErrorHandler { + @Override + protected String render(Object item) { + if (item instanceof Calendar) { + throw new IllegalArgumentException("calendar"); + } else if (item instanceof Date) { + return String.valueOf(((Date) item).getTime()); + } + return null; + } } @Test @@ -149,12 +169,86 @@ public void testOnErrorWhenNotImplemented() { assertEquals(1, errorHandler.count); } + @Test + public void testOnNextValueRenderingWhenNotImplemented() { + RxJavaErrorHandlerTestImpl errorHandler = new RxJavaErrorHandlerTestImpl(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(new Date()); + + assertNull(rendering); + } + + @Test + public void testOnNextValueRenderingWhenImplementedAndNotManaged() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering( + Collections.emptyList()); + + assertNull(rendering); + } + + @Test + public void testOnNextValueRenderingWhenImplementedAndManaged() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + long time = 1234L; + Date date = new Date(time); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(date); + + assertNotNull(rendering); + assertEquals(String.valueOf(time), rendering); + } + + @Test + public void testOnNextValueRenderingWhenImplementedAndThrows() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + Calendar cal = Calendar.getInstance(); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(cal); + + assertNotNull(rendering); + assertEquals(cal.getClass().getName() + RxJavaErrorHandler.ERROR_IN_RENDERING_SUFFIX, rendering); + } + + @Test + public void testOnNextValueCallsPlugin() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + long time = 456L; + Date date = new Date(time); + + try { + Date notExpected = Observable.just(date) + .map(new Func1() { + @Override + public Date call(Date date) { + throw new IllegalStateException("Trigger OnNextValue"); + } + }) + .timeout(500, TimeUnit.MILLISECONDS) + .toBlocking().first(); + fail("Did not expect onNext/onCompleted, got " + notExpected); + } catch (IllegalStateException e) { + assertEquals("Trigger OnNextValue", e.getMessage()); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof OnErrorThrowable.OnNextValue); + assertEquals("OnError while emitting onNext value: " + time, e.getCause().getMessage()); + } + + } + // inside test so it is stripped from Javadocs public static class RxJavaObservableExecutionHookTestImpl extends RxJavaObservableExecutionHook { // just use defaults } private static String getFullClassNameForTestClass(Class cls) { - return RxJavaPlugins.class.getPackage().getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName(); + return RxJavaPlugins.class.getPackage() + .getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName(); } } From 17ff0801f045ec11349450b1467f375b309cb04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 4 Mar 2015 23:20:08 +0000 Subject: [PATCH 188/857] statically reference the plugin and mark as experimental --- src/main/java/rx/exceptions/OnErrorThrowable.java | 5 ++++- src/main/java/rx/plugins/RxJavaErrorHandler.java | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java index 4e0969a6eb..6f736c9140 100644 --- a/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -109,6 +109,9 @@ public static Throwable addValueAsLastCause(Throwable e, Object value) { public static class OnNextValue extends RuntimeException { private static final long serialVersionUID = -3454462756050397899L; + + private static final RxJavaErrorHandler ERROR_HANDLER = RxJavaPlugins.getInstance().getErrorHandler(); + private final Object value; /** @@ -161,7 +164,7 @@ private static String renderValue(Object value){ return ((Enum) value).name(); } - String pluggedRendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(value); + String pluggedRendering = ERROR_HANDLER.handleOnNextValueRendering(value); if (pluggedRendering != null) { return pluggedRendering; } diff --git a/src/main/java/rx/plugins/RxJavaErrorHandler.java b/src/main/java/rx/plugins/RxJavaErrorHandler.java index 9dd52fdc3d..f228404419 100644 --- a/src/main/java/rx/plugins/RxJavaErrorHandler.java +++ b/src/main/java/rx/plugins/RxJavaErrorHandler.java @@ -17,6 +17,7 @@ import rx.Observable; import rx.Subscriber; +import rx.annotations.Experimental; import rx.exceptions.OnErrorThrowable; /** @@ -62,6 +63,7 @@ public void handleError(Throwable e) { * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. * @return a short {@link String} representation of the item if one is known for its type, or null for default. */ + @Experimental public final String handleOnNextValueRendering(Object item) { try { return render(item); @@ -83,6 +85,7 @@ public final String handleOnNextValueRendering(Object item) { * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. * @return a short {@link String} representation of the item if one is known for its type, or null for default. */ + @Experimental protected String render (Object item) { //do nothing by default return null; From 94b53d6b5da0d0c0b83830fea42a99cd2916241e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 5 Mar 2015 12:21:32 +0100 Subject: [PATCH 189/857] ObserveOn throughput enhancements --- .../internal/operators/OperatorObserveOn.java | 102 ++++++++++-------- .../schedulers/EventLoopsScheduler.java | 38 ++++--- .../internal/schedulers/NewThreadWorker.java | 35 +++++- .../internal/schedulers/ScheduledAction.java | 56 ++++++++-- .../rx/internal/util/SubscriptionList.java | 62 +++++++++-- .../operators/OperatorConcatTest.java | 2 +- .../operators/OperatorReplayTest.java | 2 + 7 files changed, 217 insertions(+), 80 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index b11ebd660c..af08f2a1b9 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -15,19 +15,16 @@ */ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.Queue; +import java.util.concurrent.atomic.*; import rx.Observable.Operator; -import rx.Producer; -import rx.Scheduler; -import rx.Subscriber; -import rx.Subscription; +import rx.*; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; -import rx.internal.util.RxRingBuffer; -import rx.schedulers.ImmediateScheduler; -import rx.schedulers.TrampolineScheduler; +import rx.internal.util.*; +import rx.internal.util.unsafe.*; +import rx.schedulers.*; /** * Delivers events on the specified {@code Scheduler} asynchronously via an unbounded buffer. @@ -64,16 +61,15 @@ public Subscriber call(Subscriber child) { /** Observe through individual queue per observer. */ private static final class ObserveOnSubscriber extends Subscriber { final Subscriber child; - private final Scheduler.Worker recursiveScheduler; - private final ScheduledUnsubscribe scheduledUnsubscribe; + final Scheduler.Worker recursiveScheduler; + final ScheduledUnsubscribe scheduledUnsubscribe; final NotificationLite on = NotificationLite.instance(); - private final RxRingBuffer queue = RxRingBuffer.getSpscInstance(); - private boolean completed = false; - private boolean failure = false; + final Queue queue; + volatile boolean completed = false; + volatile boolean failure = false; - @SuppressWarnings("unused") - private volatile long requested = 0; + volatile long requested = 0; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "requested"); @@ -82,12 +78,19 @@ private static final class ObserveOnSubscriber extends Subscriber { @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater COUNTER_UPDATER = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "counter"); + volatile Throwable error; + // 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) { this.child = child; this.recursiveScheduler = scheduler.createWorker(); - this.scheduledUnsubscribe = new ScheduledUnsubscribe(recursiveScheduler, queue); + if (UnsafeAccess.isUnsafeAvailable()) { + queue = new SpscArrayQueue(RxRingBuffer.SIZE); + } else { + queue = new SynchronizedQueue(RxRingBuffer.SIZE); + } + this.scheduledUnsubscribe = new ScheduledUnsubscribe(recursiveScheduler); child.add(scheduledUnsubscribe); child.setProducer(new Producer() { @@ -113,10 +116,8 @@ public void onNext(final T t) { if (isUnsubscribed() || completed) { return; } - try { - queue.onNext(t); - } catch (MissingBackpressureException e) { - onError(e); + if (!queue.offer(on.next(t))) { + onError(new MissingBackpressureException()); return; } schedule(); @@ -127,8 +128,10 @@ public void onCompleted() { if (isUnsubscribed() || completed) { return; } + if (error != null) { + return; + } completed = true; - queue.onCompleted(); schedule(); } @@ -137,53 +140,64 @@ public void onError(final Throwable e) { if (isUnsubscribed() || completed) { return; } + if (error != null) { + return; + } + error = e; // unsubscribe eagerly since time will pass before the scheduled onError results in an unsubscribe event unsubscribe(); - completed = true; // mark failure so the polling thread will skip onNext still in the queue + completed = true; failure = true; - queue.onError(e); schedule(); } - protected void schedule() { - if (COUNTER_UPDATER.getAndIncrement(this) == 0) { - recursiveScheduler.schedule(new Action0() { + final Action0 action = new Action0() { - @Override - public void call() { - pollQueue(); - } + @Override + public void call() { + pollQueue(); + } - }); + }; + + protected void schedule() { + if (COUNTER_UPDATER.getAndIncrement(this) == 0) { + recursiveScheduler.schedule(action); } } // only execute this from schedule() - private void pollQueue() { + void pollQueue() { int emitted = 0; do { /* * Set to 1 otherwise it could have grown very large while in the last poll loop * and then we can end up looping all those times again here before exiting even once we've drained */ - COUNTER_UPDATER.set(this, 1); + counter = 1; +// middle: while (!scheduledUnsubscribe.isUnsubscribed()) { if (failure) { - // special handling to short-circuit an error propagation - Object o = queue.poll(); - // completed so we will skip onNext if they exist and only emit terminal events - if (on.isError(o)) { - // only emit error - on.accept(child, o); - // we have emitted a terminal event so return (exit the loop we're in) + child.onError(error); + return; + } else { + if (requested == 0 && completed && queue.isEmpty()) { + child.onCompleted(); return; } - } else { if (REQUESTED.getAndDecrement(this) != 0) { Object o = queue.poll(); if (o == null) { + if (completed) { + if (failure) { + child.onError(error); + } else { + child.onCompleted(); + } + return; + } // nothing in queue REQUESTED.incrementAndGet(this); break; @@ -213,12 +227,10 @@ static final class ScheduledUnsubscribe implements Subscription { final Scheduler.Worker worker; volatile int once; static final AtomicIntegerFieldUpdater ONCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(ScheduledUnsubscribe.class, "once"); - final RxRingBuffer queue; volatile boolean unsubscribed = false; - public ScheduledUnsubscribe(Scheduler.Worker worker, RxRingBuffer queue) { + public ScheduledUnsubscribe(Scheduler.Worker worker) { this.worker = worker; - this.queue = queue; } @Override diff --git a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java index 91a5440227..71c4397754 100644 --- a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java @@ -15,15 +15,12 @@ */ package rx.internal.schedulers; -import rx.Scheduler; -import rx.Subscription; -import rx.functions.Action0; -import rx.internal.util.RxThreadFactory; -import rx.subscriptions.CompositeSubscription; -import rx.subscriptions.Subscriptions; +import java.util.concurrent.*; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; +import rx.*; +import rx.functions.Action0; +import rx.internal.util.*; +import rx.subscriptions.*; public class EventLoopsScheduler extends Scheduler { /** Manages a fixed number of workers. */ @@ -95,7 +92,9 @@ public Subscription scheduleDirect(Action0 action) { } private static class EventLoopWorker extends Scheduler.Worker { - private final CompositeSubscription innerSubscription = new CompositeSubscription(); + private final SubscriptionList serial = new SubscriptionList(); + private final CompositeSubscription timed = new CompositeSubscription(); + private final SubscriptionList both = new SubscriptionList(serial, timed); private final PoolWorker poolWorker; EventLoopWorker(PoolWorker poolWorker) { @@ -105,28 +104,33 @@ private static class EventLoopWorker extends Scheduler.Worker { @Override public void unsubscribe() { - innerSubscription.unsubscribe(); + both.unsubscribe(); } @Override public boolean isUnsubscribed() { - return innerSubscription.isUnsubscribed(); + return both.isUnsubscribed(); } @Override public Subscription schedule(Action0 action) { - return schedule(action, 0, null); + if (isUnsubscribed()) { + return Subscriptions.unsubscribed(); + } + ScheduledAction s = poolWorker.scheduleActual(action, 0, null); + + serial.add(s); + s.addParent(serial); + + return s; } @Override public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { - if (innerSubscription.isUnsubscribed()) { - // don't schedule, we are unsubscribed + if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } + ScheduledAction s = poolWorker.scheduleActual(action, delayTime, unit, timed); - ScheduledAction s = poolWorker.scheduleActual(action, delayTime, unit); - innerSubscription.add(s); - s.addParent(innerSubscription); return s; } } diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index 41144795cb..094c94892f 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -23,9 +23,9 @@ import rx.*; import rx.exceptions.Exceptions; import rx.functions.Action0; -import rx.internal.util.RxThreadFactory; +import rx.internal.util.*; import rx.plugins.*; -import rx.subscriptions.Subscriptions; +import rx.subscriptions.*; /** * @warn class description missing @@ -174,6 +174,37 @@ public ScheduledAction scheduleActual(final Action0 action, long delayTime, Time return run; } + public ScheduledAction scheduleActual(final Action0 action, long delayTime, TimeUnit unit, CompositeSubscription parent) { + Action0 decoratedAction = schedulersHook.onSchedule(action); + ScheduledAction run = new ScheduledAction(decoratedAction, parent); + parent.add(run); + + Future f; + if (delayTime <= 0) { + f = executor.submit(run); + } else { + f = executor.schedule(run, delayTime, unit); + } + run.add(f); + + return run; + } + + public ScheduledAction scheduleActual(final Action0 action, long delayTime, TimeUnit unit, SubscriptionList parent) { + Action0 decoratedAction = schedulersHook.onSchedule(action); + ScheduledAction run = new ScheduledAction(decoratedAction, parent); + parent.add(run); + + Future f; + if (delayTime <= 0) { + f = executor.submit(run); + } else { + f = executor.schedule(run, delayTime, unit); + } + run.add(f); + + return run; + } @Override public void unsubscribe() { diff --git a/src/main/java/rx/internal/schedulers/ScheduledAction.java b/src/main/java/rx/internal/schedulers/ScheduledAction.java index 24240096c9..8ddd18870b 100644 --- a/src/main/java/rx/internal/schedulers/ScheduledAction.java +++ b/src/main/java/rx/internal/schedulers/ScheduledAction.java @@ -16,12 +16,12 @@ package rx.internal.schedulers; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.*; import rx.Subscription; import rx.exceptions.OnErrorNotImplementedException; import rx.functions.Action0; +import rx.internal.util.SubscriptionList; import rx.plugins.RxJavaPlugins; import rx.subscriptions.CompositeSubscription; @@ -32,12 +32,20 @@ public final class ScheduledAction extends AtomicReference implements Runnable, Subscription { /** */ private static final long serialVersionUID = -3962399486978279857L; - final CompositeSubscription cancel; + final SubscriptionList cancel; final Action0 action; public ScheduledAction(Action0 action) { this.action = action; - this.cancel = new CompositeSubscription(); + this.cancel = new SubscriptionList(); + } + public ScheduledAction(Action0 action, CompositeSubscription parent) { + this.action = action; + this.cancel = new SubscriptionList(new Remover(this, parent)); + } + public ScheduledAction(Action0 action, SubscriptionList parent) { + this.action = action; + this.cancel = new SubscriptionList(new Remover2(this, parent)); } @Override @@ -103,6 +111,17 @@ public void addParent(CompositeSubscription parent) { cancel.add(new Remover(this, parent)); } + /** + * Adds a parent {@link CompositeSubscription} to this {@code ScheduledAction} so when the action is + * cancelled or terminates, it can remove itself from this parent. + * + * @param parent + * the parent {@code CompositeSubscription} to add + */ + public void addParent(SubscriptionList parent) { + cancel.add(new Remover2(this, parent)); + } + /** * Cancels the captured future if the caller of the call method * is not the same as the runner of the outer ScheduledAction to @@ -134,10 +153,35 @@ public boolean isUnsubscribed() { private static final class Remover extends AtomicBoolean implements Subscription { /** */ private static final long serialVersionUID = 247232374289553518L; - final Subscription s; + final ScheduledAction s; final CompositeSubscription parent; - public Remover(Subscription s, CompositeSubscription parent) { + public Remover(ScheduledAction s, CompositeSubscription parent) { + this.s = s; + this.parent = parent; + } + + @Override + public boolean isUnsubscribed() { + return s.isUnsubscribed(); + } + + @Override + public void unsubscribe() { + if (compareAndSet(false, true)) { + parent.remove(s); + } + } + + } + /** Remove a child subscription from a composite when unsubscribing. */ + private static final class Remover2 extends AtomicBoolean implements Subscription { + /** */ + private static final long serialVersionUID = 247232374289553518L; + final ScheduledAction s; + final SubscriptionList parent; + + public Remover2(ScheduledAction s, SubscriptionList parent) { this.s = s; this.parent = parent; } diff --git a/src/main/java/rx/internal/util/SubscriptionList.java b/src/main/java/rx/internal/util/SubscriptionList.java index 7583131fda..a3a91fa1b0 100644 --- a/src/main/java/rx/internal/util/SubscriptionList.java +++ b/src/main/java/rx/internal/util/SubscriptionList.java @@ -20,9 +20,10 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.locks.ReentrantLock; import rx.Subscription; -import rx.exceptions.*; +import rx.exceptions.Exceptions; /** * Subscription that represents a group of Subscriptions that are unsubscribed together. @@ -31,8 +32,9 @@ */ public final class SubscriptionList implements Subscription { - private List subscriptions; + private LinkedList subscriptions; private volatile boolean unsubscribed; + private final ReentrantLock lock = new ReentrantLock(); public SubscriptionList() { } @@ -41,6 +43,11 @@ public SubscriptionList(final Subscription... subscriptions) { this.subscriptions = new LinkedList(Arrays.asList(subscriptions)); } + public SubscriptionList(Subscription s) { + this.subscriptions = new LinkedList(); + this.subscriptions.add(s); + } + @Override public boolean isUnsubscribed() { return unsubscribed; @@ -55,21 +62,49 @@ public boolean isUnsubscribed() { * the {@link Subscription} to add */ public void add(final Subscription s) { + if (s.isUnsubscribed()) { + return; + } if (!unsubscribed) { - synchronized (this) { + lock.lock(); + try { if (!unsubscribed) { - if (subscriptions == null) { - subscriptions = new LinkedList(); + LinkedList subs = subscriptions; + if (subs == null) { + subs = new LinkedList(); + subscriptions = subs; } - subscriptions.add(s); + subs.add(s); return; } + } finally { + lock.unlock(); } } // call after leaving the synchronized block so we're not holding a lock while executing this s.unsubscribe(); } + public void remove(final Subscription s) { + if (!unsubscribed) { + boolean unsubscribe = false; + lock.lock(); + try { + LinkedList subs = subscriptions; + if (unsubscribed || subs == null) { + return; + } + unsubscribe = subs.remove(s); + } finally { + lock.unlock(); + } + if (unsubscribe) { + // if we removed successfully we then need to call unsubscribe on it (outside of the lock) + s.unsubscribe(); + } + } + } + /** * Unsubscribe from all of the subscriptions in the list, which stops the receipt of notifications on * the associated {@code Subscriber}. @@ -78,13 +113,16 @@ public void add(final Subscription s) { public void unsubscribe() { if (!unsubscribed) { List list; - synchronized (this) { + lock.lock(); + try { if (unsubscribed) { return; } unsubscribed = true; list = subscriptions; subscriptions = null; + } finally { + lock.unlock(); } // we will only get here once unsubscribeFromAll(list); @@ -112,9 +150,12 @@ private static void unsubscribeFromAll(Collection subscriptions) { public void clear() { if (!unsubscribed) { List list; - synchronized (this) { + lock.lock(); + try { list = subscriptions; subscriptions = null; + } finally { + lock.unlock(); } unsubscribeFromAll(list); } @@ -125,8 +166,11 @@ public void clear() { */ public boolean hasSubscriptions() { if (!unsubscribed) { - synchronized (this) { + lock.lock(); + try { return !unsubscribed && subscriptions != null && !subscriptions.isEmpty(); + } finally { + lock.unlock(); } } return false; diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 9c457e41b0..53b6f320a9 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -471,7 +471,7 @@ public void unsubscribe() { @Override public boolean isUnsubscribed() { - return subscribed; + return !subscribed; } }; diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 8e6dddce8c..bd15f03b8b 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -607,6 +607,7 @@ public void testIssue2191_SchedulerUnsubscribe() throws Exception { verifyObserverMock(mockObserverBeforeConnect, 2, 6); verifyObserverMock(mockObserverAfterConnect, 2, 6); + verify(spiedWorker, times(1)).isUnsubscribed(); verify(spiedWorker, times(1)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); @@ -666,6 +667,7 @@ public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { verifyObserver(mockObserverBeforeConnect, 2, 2, illegalArgumentException); verifyObserver(mockObserverAfterConnect, 2, 2, illegalArgumentException); + verify(spiedWorker, times(1)).isUnsubscribed(); verify(spiedWorker, times(1)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); From c286285238127ddc10bc2234583516b4ddeacd1a Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Thu, 5 Mar 2015 19:19:20 +0300 Subject: [PATCH 190/857] Corrected all Java interfaces declarations --- src/main/java/rx/Observable.java | 4 ++-- src/main/java/rx/Observer.java | 6 +++--- src/main/java/rx/Producer.java | 2 +- src/main/java/rx/Subscription.java | 4 ++-- src/main/java/rx/functions/Action0.java | 2 +- src/main/java/rx/functions/Action1.java | 2 +- src/main/java/rx/functions/Action2.java | 2 +- src/main/java/rx/functions/Action3.java | 2 +- src/main/java/rx/functions/Func0.java | 2 +- src/main/java/rx/functions/Func1.java | 2 +- src/main/java/rx/functions/Func2.java | 2 +- src/main/java/rx/functions/Func3.java | 2 +- src/main/java/rx/functions/Func4.java | 2 +- src/main/java/rx/functions/Func5.java | 2 +- src/main/java/rx/functions/Func6.java | 2 +- src/main/java/rx/functions/Func7.java | 2 +- src/main/java/rx/functions/Func8.java | 2 +- src/main/java/rx/functions/Func9.java | 2 +- src/main/java/rx/functions/FuncN.java | 2 +- .../java/rx/internal/operators/OperatorTimeoutBase.java | 4 ++-- .../java/rx/internal/operators/OnSubscribeUsingTest.java | 2 +- 21 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index b65fd94f5b..7ed81fea06 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -98,7 +98,7 @@ public final static Observable create(OnSubscribe f) { /** * Invoked when Obserable.subscribe is called. */ - public static interface OnSubscribe extends Action1> { + public interface OnSubscribe extends Action1> { // cover for generics insanity } @@ -191,7 +191,7 @@ public Observable compose(Transformer transformer * Transformer function used by {@link #compose}. * @warn more complete description needed */ - public static interface Transformer extends Func1, Observable> { + public interface Transformer extends Func1, Observable> { // cover for generics insanity } diff --git a/src/main/java/rx/Observer.java b/src/main/java/rx/Observer.java index 22f4de9704..8c7eca5c77 100644 --- a/src/main/java/rx/Observer.java +++ b/src/main/java/rx/Observer.java @@ -34,7 +34,7 @@ public interface Observer { *

* The {@link Observable} will not call this method if it calls {@link #onError}. */ - public abstract void onCompleted(); + void onCompleted(); /** * Notifies the Observer that the {@link Observable} has experienced an error condition. @@ -45,7 +45,7 @@ public interface Observer { * @param e * the exception encountered by the Observable */ - public abstract void onError(Throwable e); + void onError(Throwable e); /** * Provides the Observer with a new item to observe. @@ -58,6 +58,6 @@ public interface Observer { * @param t * the item emitted by the Observable */ - public abstract void onNext(T t); + void onNext(T t); } diff --git a/src/main/java/rx/Producer.java b/src/main/java/rx/Producer.java index 37f88d1861..4d9f4428fd 100644 --- a/src/main/java/rx/Producer.java +++ b/src/main/java/rx/Producer.java @@ -37,6 +37,6 @@ public interface Producer { * @param n the maximum number of items you want this Producer to produce, or {@code Long.MAX_VALUE} if you * want the Producer to produce items at its own pace */ - public void request(long n); + void request(long n); } diff --git a/src/main/java/rx/Subscription.java b/src/main/java/rx/Subscription.java index aefa9d7f21..00358903c4 100644 --- a/src/main/java/rx/Subscription.java +++ b/src/main/java/rx/Subscription.java @@ -33,13 +33,13 @@ public interface Subscription { * This allows unregistering an {@link Subscriber} before it has finished receiving all events (i.e. before * onCompleted is called). */ - public void unsubscribe(); + void unsubscribe(); /** * Indicates whether this {@code Subscription} is currently unsubscribed. * * @return {@code true} if this {@code Subscription} is currently unsubscribed, {@code false} otherwise */ - public boolean isUnsubscribed(); + boolean isUnsubscribed(); } diff --git a/src/main/java/rx/functions/Action0.java b/src/main/java/rx/functions/Action0.java index 9840633327..cf11288d28 100644 --- a/src/main/java/rx/functions/Action0.java +++ b/src/main/java/rx/functions/Action0.java @@ -19,5 +19,5 @@ * A zero-argument action. */ public interface Action0 extends Action { - public void call(); + void call(); } diff --git a/src/main/java/rx/functions/Action1.java b/src/main/java/rx/functions/Action1.java index ef9ac8f166..e660036d5d 100644 --- a/src/main/java/rx/functions/Action1.java +++ b/src/main/java/rx/functions/Action1.java @@ -19,5 +19,5 @@ * A one-argument action. */ public interface Action1 extends Action { - public void call(T1 t1); + void call(T1 t1); } diff --git a/src/main/java/rx/functions/Action2.java b/src/main/java/rx/functions/Action2.java index 8ff47a378b..b22cd77911 100644 --- a/src/main/java/rx/functions/Action2.java +++ b/src/main/java/rx/functions/Action2.java @@ -19,5 +19,5 @@ * A two-argument action. */ public interface Action2 extends Action { - public void call(T1 t1, T2 t2); + void call(T1 t1, T2 t2); } diff --git a/src/main/java/rx/functions/Action3.java b/src/main/java/rx/functions/Action3.java index 597490b6f9..56a9dd64a9 100644 --- a/src/main/java/rx/functions/Action3.java +++ b/src/main/java/rx/functions/Action3.java @@ -19,5 +19,5 @@ * A three-argument action. */ public interface Action3 extends Action { - public void call(T1 t1, T2 t2, T3 t3); + void call(T1 t1, T2 t2, T3 t3); } diff --git a/src/main/java/rx/functions/Func0.java b/src/main/java/rx/functions/Func0.java index a7ba8bf76c..96df934066 100644 --- a/src/main/java/rx/functions/Func0.java +++ b/src/main/java/rx/functions/Func0.java @@ -22,5 +22,5 @@ */ public interface Func0 extends Function, Callable { @Override - public R call(); + R call(); } diff --git a/src/main/java/rx/functions/Func1.java b/src/main/java/rx/functions/Func1.java index 9aa142ec4b..eb5d64fd24 100644 --- a/src/main/java/rx/functions/Func1.java +++ b/src/main/java/rx/functions/Func1.java @@ -19,5 +19,5 @@ * Represents a function with one argument. */ public interface Func1 extends Function { - public R call(T1 t1); + R call(T1 t1); } diff --git a/src/main/java/rx/functions/Func2.java b/src/main/java/rx/functions/Func2.java index c5fbae9a09..b44aeed76e 100644 --- a/src/main/java/rx/functions/Func2.java +++ b/src/main/java/rx/functions/Func2.java @@ -19,5 +19,5 @@ * Represents a function with two arguments. */ public interface Func2 extends Function { - public R call(T1 t1, T2 t2); + R call(T1 t1, T2 t2); } diff --git a/src/main/java/rx/functions/Func3.java b/src/main/java/rx/functions/Func3.java index 978edf820c..a47cbaf893 100644 --- a/src/main/java/rx/functions/Func3.java +++ b/src/main/java/rx/functions/Func3.java @@ -19,5 +19,5 @@ * Represents a function with three arguments. */ public interface Func3 extends Function { - public R call(T1 t1, T2 t2, T3 t3); + R call(T1 t1, T2 t2, T3 t3); } diff --git a/src/main/java/rx/functions/Func4.java b/src/main/java/rx/functions/Func4.java index 9b3332787b..b07ea63d99 100644 --- a/src/main/java/rx/functions/Func4.java +++ b/src/main/java/rx/functions/Func4.java @@ -19,5 +19,5 @@ * Represents a function with four arguments. */ public interface Func4 extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4); + R call(T1 t1, T2 t2, T3 t3, T4 t4); } diff --git a/src/main/java/rx/functions/Func5.java b/src/main/java/rx/functions/Func5.java index 8b1c29af7a..30016c7eaf 100644 --- a/src/main/java/rx/functions/Func5.java +++ b/src/main/java/rx/functions/Func5.java @@ -19,5 +19,5 @@ * Represents a function with five arguments. */ public interface Func5 extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5); + R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5); } diff --git a/src/main/java/rx/functions/Func6.java b/src/main/java/rx/functions/Func6.java index 0eb01ab224..e74af30733 100644 --- a/src/main/java/rx/functions/Func6.java +++ b/src/main/java/rx/functions/Func6.java @@ -19,5 +19,5 @@ * Represents a function with six arguments. */ public interface Func6 extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6); + R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6); } diff --git a/src/main/java/rx/functions/Func7.java b/src/main/java/rx/functions/Func7.java index 848eee880b..181e504df0 100644 --- a/src/main/java/rx/functions/Func7.java +++ b/src/main/java/rx/functions/Func7.java @@ -19,5 +19,5 @@ * Represents a function with seven arguments. */ public interface Func7 extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7); + R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7); } diff --git a/src/main/java/rx/functions/Func8.java b/src/main/java/rx/functions/Func8.java index d91332f1e2..1f49817d12 100644 --- a/src/main/java/rx/functions/Func8.java +++ b/src/main/java/rx/functions/Func8.java @@ -19,5 +19,5 @@ * Represents a function with eight arguments. */ public interface Func8 extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8); + R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8); } diff --git a/src/main/java/rx/functions/Func9.java b/src/main/java/rx/functions/Func9.java index 4d6e664f2f..56deac6119 100644 --- a/src/main/java/rx/functions/Func9.java +++ b/src/main/java/rx/functions/Func9.java @@ -19,5 +19,5 @@ * Represents a function with nine arguments. */ public interface Func9 extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9); + R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9); } diff --git a/src/main/java/rx/functions/FuncN.java b/src/main/java/rx/functions/FuncN.java index 3407eb4dab..72d1742d40 100644 --- a/src/main/java/rx/functions/FuncN.java +++ b/src/main/java/rx/functions/FuncN.java @@ -19,5 +19,5 @@ * Represents a vector-argument function. */ public interface FuncN extends Function { - public R call(Object... args); + R call(Object... args); } diff --git a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java index 0e294dc531..038bf88a0c 100644 --- a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java +++ b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java @@ -36,7 +36,7 @@ class OperatorTimeoutBase implements Operator { * * @param */ - /* package-private */static interface FirstTimeoutStub extends + /* package-private */interface FirstTimeoutStub extends Func3, Long, Scheduler.Worker, Subscription> { } @@ -45,7 +45,7 @@ class OperatorTimeoutBase implements Operator { * * @param */ - /* package-private */static interface TimeoutStub extends + /* package-private */interface TimeoutStub extends Func4, Long, T, Scheduler.Worker, Subscription> { } diff --git a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java index c66e43e246..03d4cfd24a 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java @@ -44,7 +44,7 @@ public class OnSubscribeUsingTest { - private static interface Resource { + private interface Resource { public String getTextFromWeb(); public void dispose(); From d6eb4dd0ff659c7132e054e9623ea85745f9548c Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 5 Mar 2015 22:18:52 +0100 Subject: [PATCH 191/857] Fixed takeUntil not unsubscribing from either of the observables in case of a terminal condition. --- .../internal/operators/OperatorTakeUntil.java | 52 +++++++-- .../operators/OperatorTakeUntilTest.java | 104 +++++++++++++++++- 2 files changed, 144 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorTakeUntil.java b/src/main/java/rx/internal/operators/OperatorTakeUntil.java index 11fd5572a9..9c0f131b1f 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeUntil.java +++ b/src/main/java/rx/internal/operators/OperatorTakeUntil.java @@ -36,28 +36,62 @@ public OperatorTakeUntil(final Observable other) { @Override public Subscriber call(final Subscriber child) { - final Subscriber parent = new SerializedSubscriber(child); - - other.unsafeSubscribe(new Subscriber(child) { - + final Subscriber serial = new SerializedSubscriber(child, false); + + final Subscriber main = new Subscriber(serial, false) { + @Override + public void onNext(T t) { + serial.onNext(t); + } + @Override + public void onError(Throwable e) { + try { + serial.onError(e); + } finally { + serial.unsubscribe(); + } + } @Override public void onCompleted() { - parent.onCompleted(); + try { + serial.onCompleted(); + } finally { + serial.unsubscribe(); + } + } + }; + + final Subscriber so = new Subscriber() { + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onCompleted() { + main.onCompleted(); } @Override public void onError(Throwable e) { - parent.onError(e); + main.onError(e); } @Override public void onNext(E t) { - parent.onCompleted(); + onCompleted(); } - }); + }; + + serial.add(main); + serial.add(so); + + child.add(serial); + + other.unsafeSubscribe(so); - return parent; + return main; } } diff --git a/src/test/java/rx/internal/operators/OperatorTakeUntilTest.java b/src/test/java/rx/internal/operators/OperatorTakeUntilTest.java index 2706c99676..1667d306f8 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeUntilTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeUntilTest.java @@ -15,9 +15,11 @@ */ package rx.internal.operators; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +import java.util.Arrays; import org.junit.Test; @@ -25,6 +27,8 @@ import rx.Observer; import rx.Subscriber; import rx.Subscription; +import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; public class OperatorTakeUntilTest { @@ -188,4 +192,98 @@ public void call(Subscriber observer) { observer.add(s); } } + + @Test + public void testUntilFires() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.takeUntil(until).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + + ts.assertReceivedOnNext(Arrays.asList(1)); + until.onNext(1); + + ts.assertReceivedOnNext(Arrays.asList(1)); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } + @Test + public void testMainCompletes() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.takeUntil(until).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + source.onCompleted(); + + ts.assertReceivedOnNext(Arrays.asList(1)); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } + @Test + public void testDownstreamUnsubscribes() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.takeUntil(until).take(1).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + + ts.assertReceivedOnNext(Arrays.asList(1)); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } + public void testBackpressure() { + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + requestMore(0); + } + }; + + Observable.range(1, 10).takeUntil(until).unsafeSubscribe(ts); + + assertTrue(until.hasObservers()); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Arrays.asList(1)); + ts.assertNoErrors(); + assertTrue("TestSubscriber completed", ts.getOnCompletedEvents().isEmpty()); + + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } } From c4b92ef3b658db5ae0c4e0b764797adbf98f8963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 6 Mar 2015 18:37:16 +0000 Subject: [PATCH 192/857] revert eager static ref to plugin + better exception handling around render --- src/main/java/rx/exceptions/OnErrorThrowable.java | 4 +--- src/main/java/rx/plugins/RxJavaErrorHandler.java | 10 ++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java index 6f736c9140..dc34843868 100644 --- a/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -110,8 +110,6 @@ public static class OnNextValue extends RuntimeException { private static final long serialVersionUID = -3454462756050397899L; - private static final RxJavaErrorHandler ERROR_HANDLER = RxJavaPlugins.getInstance().getErrorHandler(); - private final Object value; /** @@ -164,7 +162,7 @@ private static String renderValue(Object value){ return ((Enum) value).name(); } - String pluggedRendering = ERROR_HANDLER.handleOnNextValueRendering(value); + String pluggedRendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(value); if (pluggedRendering != null) { return pluggedRendering; } diff --git a/src/main/java/rx/plugins/RxJavaErrorHandler.java b/src/main/java/rx/plugins/RxJavaErrorHandler.java index f228404419..62fd222bf5 100644 --- a/src/main/java/rx/plugins/RxJavaErrorHandler.java +++ b/src/main/java/rx/plugins/RxJavaErrorHandler.java @@ -18,6 +18,7 @@ import rx.Observable; import rx.Subscriber; import rx.annotations.Experimental; +import rx.exceptions.Exceptions; import rx.exceptions.OnErrorThrowable; /** @@ -65,11 +66,15 @@ public void handleError(Throwable e) { */ @Experimental public final String handleOnNextValueRendering(Object item) { + try { return render(item); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } catch (Throwable t) { - return item.getClass().getName() + ERROR_IN_RENDERING_SUFFIX; + Exceptions.throwIfFatal(t); } + return item.getClass().getName() + ERROR_IN_RENDERING_SUFFIX; } /** @@ -84,9 +89,10 @@ public final String handleOnNextValueRendering(Object item) { * * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. * @return a short {@link String} representation of the item if one is known for its type, or null for default. + * @throws InterruptedException if the rendering thread is interrupted */ @Experimental - protected String render (Object item) { + protected String render (Object item) throws InterruptedException { //do nothing by default return null; } From a917b91602f961aa12e916282c7e0770e25ec9cc Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Sat, 7 Mar 2015 12:51:23 -0800 Subject: [PATCH 193/857] 1.0.8 --- CHANGES.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0a661b49ef..738534fbe2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,18 @@ # RxJava Releases # -### Version 1.0.7 – Feburary 21st 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.7%7C)) ### +### Version 1.0.8 – March 7th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.8%7C)) ### + +* [Pull 2809] (https://github.com/ReactiveX/RxJava/pull/2809) Fixed takeUntil not unsubscribing from either of the observables in case of a terminal condition. +* [Pull 2804] (https://github.com/ReactiveX/RxJava/pull/2804) ObserveOn throughput enhancements +* [Pull 2767] (https://github.com/ReactiveX/RxJava/pull/2767) Optimized scalar observeOn/subscribeOn +* [Pull 2776] (https://github.com/ReactiveX/RxJava/pull/2776) Experimental: add new operator onBackpressureDrop(Action1 onDrop) +* [Pull 2788] (https://github.com/ReactiveX/RxJava/pull/2788) Fix the bug that 'publish' will cache items when no subscriber +* [Pull 2779] (https://github.com/ReactiveX/RxJava/pull/2779) OperatorMulticast.connect(connection) should not return null +* [Pull 2771] (https://github.com/ReactiveX/RxJava/pull/2771) OnSubscribeRange request overflow check +* [Pull 2770] (https://github.com/ReactiveX/RxJava/pull/2770) OperatorOnBackpressureDrop request overflow check +* [Pull 2769] (https://github.com/ReactiveX/RxJava/pull/2769) OperatorCombineLatest request overflow check + +### Version 1.0.7 – February 21st 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.7%7C)) ### This release includes some bug fixes along with a new operator and performance enhancements. @@ -44,7 +56,7 @@ https://github.com/ReactiveX/RxJava/pull/2762 Optimized isUnsubscribed check https://github.com/ReactiveX/RxJava/pull/2759 Observable.using should use unsafeSubscribe and enable eager disposal https://github.com/ReactiveX/RxJava/pull/2655 SwitchOnNext: fix upstream producer replacing the ops own producer -### Version 1.0.6 – Feburary 11th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.6%7C)) ### +### Version 1.0.6 – February 11th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.6%7C)) ### This release adds an experimental operator and fixes several bugs. @@ -91,7 +103,7 @@ range(1, 1000000) * [Pull 2607] (https://github.com/ReactiveX/RxJava/pull/2607) OnSubscribeRefCount - improve comments -### Version 1.0.5 – Feburary 3rd 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.5%7C)) ### +### Version 1.0.5 – February 3rd 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.5%7C)) ### This release includes many bug fixes along with a few new operators and enhancements. From 1f8b8488b860e865326842c6efeb40b37267626c Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 10 Mar 2015 16:16:49 -0700 Subject: [PATCH 194/857] 800kb jar. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ab5a2c7ab..a590c9d925 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ RxJava is a Java VM implementation of [Reactive Extensions](http://reactivex.io) It extends the [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) to support sequences of data/events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety and concurrent data structures. - Zero Dependencies -- < 700KB Jar +- < 800KB Jar - Java 6+ & [Android](https://github.com/ReactiveX/RxAndroid) 2.3+ - Java 8 lambda support - Polyglot ([Scala](https://github.com/ReactiveX/RxScala), [Groovy](https://github.com/ReactiveX/RxGroovy), [Clojure](https://github.com/ReactiveX/RxClojure) and [Kotlin](https://github.com/ReactiveX/RxKotlin)) From bf941d5da0c82b95f475f41eea9698bfdad87fb3 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 11 Mar 2015 16:05:38 +0100 Subject: [PATCH 195/857] Added more to take to decrease the likelyhood that nothing is dropped. --- src/test/java/rx/BackpressureTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index 799564d50d..ffa2e01129 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -446,7 +446,7 @@ public void testOnBackpressureDropWithAction() { final AtomicInteger emitCount = new AtomicInteger(); final AtomicInteger dropCount = new AtomicInteger(); final AtomicInteger passCount = new AtomicInteger(); - final int NUM = (int) (RxRingBuffer.SIZE * 1.5); // > 1 so that take doesn't prevent buffer overflow + final int NUM = RxRingBuffer.SIZE * 3; // > 1 so that take doesn't prevent buffer overflow TestSubscriber ts = new TestSubscriber(); firehose(emitCount).onBackpressureDrop(new Action1() { @Override From 9eba5879daf3aea1d83f53b617413b1fd9d5051d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 12 Mar 2015 10:28:34 +0100 Subject: [PATCH 196/857] Small fix in the sentences of getValue. --- src/main/java/rx/subjects/AsyncSubject.java | 2 +- src/main/java/rx/subjects/BehaviorSubject.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index 4861c8b91f..5d668f1be6 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -167,7 +167,7 @@ public boolean hasCompleted() { /** * Returns the current value of the Subject if there is such a value and * the subject hasn't terminated with an exception. - *

The can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} + *

The method can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an * exception or the Subject terminated without receiving any value. * @return the current value or {@code null} if the Subject doesn't have a value, diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index 29936431c1..f2a47a8d17 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -202,7 +202,7 @@ public boolean hasCompleted() { /** * Returns the current value of the Subject if there is such a value and * the subject hasn't terminated yet. - *

The can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} + *

The method can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an * exception or the Subject terminated (with or without receiving any value). * @return the current value or {@code null} if the Subject doesn't have a value, From c698a61a0f50e69239b51a12061709e51136d48c Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 12 Mar 2015 14:29:05 +0100 Subject: [PATCH 197/857] Backpressure for window(size) --- .../operators/OperatorWindowWithSize.java | 38 +++++++----- .../operators/OperatorWindowWithSizeTest.java | 60 ++++++++++++++++--- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java index 83d62fb995..ed22a68bd6 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java @@ -15,18 +15,14 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import java.util.*; -import rx.Observable; +import rx.*; import rx.Observable.Operator; -import rx.Subscription; +import rx.Observable; +import rx.Observer; import rx.functions.Action0; import rx.subscriptions.Subscriptions; -import rx.Observer; -import rx.Subscriber; /** * Creates windows of values into the source sequence with skip frequency and size bounds. @@ -78,26 +74,36 @@ public ExactSubscriber(Subscriber> child) { @Override public void call() { // if no window we unsubscribe up otherwise wait until window ends - if(noWindow) { + if (noWindow) { parentSubscription.unsubscribe(); } } })); - } - - @Override - public void onStart() { - // no backpressure as we are controlling data flow by window size - request(Long.MAX_VALUE); + 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); + } + } + }); } + void requestMore(long n) { + request(n); + } + @Override public void onNext(T t) { if (window == null) { noWindow = false; window = BufferUntilSubscriber.create(); - child.onNext(window); + child.onNext(window); } window.onNext(t); if (++count % size == 0) { diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java index f23da00145..ed8333e5ec 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java @@ -15,20 +15,19 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; +import static org.mockito.Mockito.*; +import rx.*; import rx.Observable; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.Observer; +import rx.functions.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -198,5 +197,52 @@ private List list(String... args) { } return list; } + + @Test + public void testBackpressureOuter() { + Observable> source = Observable.range(1, 10).window(3); + + final List list = new ArrayList(); + + @SuppressWarnings("unchecked") + final Observer o = mock(Observer.class); + + source.subscribe(new Subscriber>() { + @Override + public void onStart() { + request(1); + } + @Override + public void onNext(Observable t) { + t.subscribe(new Observer() { + @Override + public void onNext(Integer t) { + list.add(t); + } + @Override + public void onError(Throwable e) { + o.onError(e); + } + @Override + public void onCompleted() { + o.onCompleted(); + } + }); + } + @Override + public void onError(Throwable e) { + o.onError(e); + } + @Override + public void onCompleted() { + o.onCompleted(); + } + }); + + assertEquals(Arrays.asList(1, 2, 3), list); + + verify(o, never()).onError(any(Throwable.class)); + verify(o, times(1)).onCompleted(); // 1 inner + } } \ No newline at end of file From 68a356ed40e1586b3bfa7661e6e32203d3edce17 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 13 Mar 2015 08:29:12 +0100 Subject: [PATCH 198/857] Changed javadoc regarding backpressure --- src/main/java/rx/Observable.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index b65fd94f5b..1d70a39bb1 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -8896,7 +8896,8 @@ public final Observable> window(Func0 *

*
Backpressure Support:
- *
This operator does not support backpressure as it uses {@code count} to control data flow.
+ *
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.
*
Scheduler:
*
This version of {@code window} does not operate by default on a particular {@link Scheduler}.
*
@@ -8920,7 +8921,8 @@ public final Observable> window(int count) { * *
*
Backpressure Support:
- *
This operator does not support backpressure as it uses {@code count} to control data flow.
+ *
The operator has limited backpressure support. If {@code count} == {@code skip}, 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.
*
Scheduler:
*
This version of {@code window} does not operate by default on a particular {@link Scheduler}.
*
From b30548b294a7f09103e598cdbe69ff145485fdcb Mon Sep 17 00:00:00 2001 From: Rob Spieldenner Date: Fri, 13 Mar 2015 15:23:41 -0700 Subject: [PATCH 199/857] Enable maven central sync via bintray --- .travis.yml | 2 ++ gradle/buildViaTravis.sh | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8704714547..35cfbac86f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,3 +14,5 @@ env: global: - secure: "HzRt91B6CLkBCxRXlo7V+F5L8SSHSW8dgkO8nAlTnOIK73k3QelDeBlcm1wnuNa3x+54YS9WBv6QrTNZr9lVi/8IPfKpd+jtjIbCWRvh6aNhqLIXWTL7oTvUd4E8DDUAKB6UMae6SiBSy2wsFELGHXeNwg7EiPfxsd5sKRiS7H4=" - secure: "MSZLPasqNKAC+8qhKQD3xO+ZbuOy65HpUN+1+KnoOLMkHCu/f4x60W1tpTAzn1DFEVpokHR0n3I4z4HpWybURDQfDHD1bB4IsznjCUBYA9Uo9Sb0U4TS17dQr8s7SORIjHDLGNSWETJjrA9TfuUV6HTVhRO1ECx3H+wuTwCVDN0=" + - secure: Joj/k9B4q1BttgP7rY1DFR9flURcvT2b4PFnxYwxljQuu6NHwz/3yLM1b711Kv9oAXlo1D/ZTXsCzle8tLs5yC3GakDCpapqZP4Gmen4zGLuHB851gejH134dJj4bEWigrSM6NJMzjbl7qmlMAc8R+DlLi/J7AxNicOrhOT5MGw= + - secure: jxpzSkzSBnTqlAAY6r8QmX4b/Gf36NTshQ7xWQ8UWkWHHjm4GTnCoR71nXCIqhtZRgXvteR2AKYbraXU3ROGkZZXR4KkEwjhkf2FVr16bmUWbiqQrVvIdBPljcV9m3OevNEzCqd3QPod/Jma5s8WIDvuOv2z/cnpN/HQiHaRFEM= diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index b8243106d2..d98e5eb603 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -6,10 +6,10 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" build snapshot --stacktrace + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' - ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" final --stacktrace + ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final --stacktrace else echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' ./gradlew -Prelease.useLastTag=true build From 00e58f40b52837e875a8cfd6dabdca48b44a24bc Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 17 Mar 2015 10:09:38 -0700 Subject: [PATCH 200/857] javadocs: adding @since annotation; adjusting failed @link targets to @code --- .../java/rx/plugins/RxJavaErrorHandler.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/plugins/RxJavaErrorHandler.java b/src/main/java/rx/plugins/RxJavaErrorHandler.java index 62fd222bf5..1003159bdb 100644 --- a/src/main/java/rx/plugins/RxJavaErrorHandler.java +++ b/src/main/java/rx/plugins/RxJavaErrorHandler.java @@ -28,7 +28,7 @@ * For example, all {@code Exception}s can be logged using this handler even if * {@link Subscriber#onError(Throwable)} is ignored or not provided when an {@link Observable} is subscribed to. *

- * This plugin is also responsible for augmenting rendering of {@link OnErrorThrowable.OnNextValue}. + * This plugin is also responsible for augmenting rendering of {@code OnErrorThrowable.OnNextValue}. *

* See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: https://github.com/ReactiveX/RxJava/wiki/Plugins. @@ -52,17 +52,21 @@ public void handleError(Throwable e) { protected static final String ERROR_IN_RENDERING_SUFFIX = ".errorRendering"; /** - * Receives items causing {@link OnErrorThrowable.OnNextValue} and gives a chance to choose the String - * representation of the item in the OnNextValue stacktrace rendering. Returns null if this type of item - * is not managed and should use default rendering. + * Receives items causing {@code OnErrorThrowable.OnNextValue} and gives a chance to choose the String + * representation of the item in the {@code OnNextValue} stacktrace rendering. Returns {@code null} if this + * type of item is not managed and should use default rendering. *

- * Note that primitive types are always rendered as their toString() value. + * Note that primitive types are always rendered as their {@code toString()} value. *

* If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by * {@value #ERROR_IN_RENDERING_SUFFIX}. * - * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. - * @return a short {@link String} representation of the item if one is known for its type, or null for default. + * @param item the last emitted item, that caused the exception wrapped in + * {@code OnErrorThrowable.OnNextValue} + * @return a short {@link String} representation of the item if one is known for its type, or null for + * default + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the + * release number) */ @Experimental public final String handleOnNextValueRendering(Object item) { @@ -80,16 +84,20 @@ public final String handleOnNextValueRendering(Object item) { /** * Override this method to provide rendering for specific types other than primitive types and null. *

- * For performance and overhead reasons, this should should limit to a safe production of a short {@code String} - * (as large renderings will bloat up the stacktrace). Prefer to try/catch({@code Throwable}) all code - * inside this method implementation. + * For performance and overhead reasons, this should should limit to a safe production of a short + * {@code String} (as large renderings will bloat up the stacktrace). Prefer to try/catch({@code Throwable}) + * all code inside this method implementation. *

* If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by * {@value #ERROR_IN_RENDERING_SUFFIX}. * - * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. - * @return a short {@link String} representation of the item if one is known for its type, or null for default. + * @param item the last emitted item, that caused the exception wrapped in + * {@code OnErrorThrowable.OnNextValue} + * @return a short {@link String} representation of the item if one is known for its type, or null for + * default * @throws InterruptedException if the rendering thread is interrupted + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the + * release number) */ @Experimental protected String render (Object item) throws InterruptedException { From 55ffd8b3a6865370ba272e3b3f0d37d2c8af1bb9 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 17 Mar 2015 18:27:50 +0100 Subject: [PATCH 201/857] Checking if the test change is the main cause or not. --- src/test/java/rx/BackpressureTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index ffa2e01129..58f3c147a3 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -446,7 +446,7 @@ public void testOnBackpressureDropWithAction() { final AtomicInteger emitCount = new AtomicInteger(); final AtomicInteger dropCount = new AtomicInteger(); final AtomicInteger passCount = new AtomicInteger(); - final int NUM = RxRingBuffer.SIZE * 3; // > 1 so that take doesn't prevent buffer overflow + final int NUM = RxRingBuffer.SIZE * 3 / 2; // > 1 so that take doesn't prevent buffer overflow TestSubscriber ts = new TestSubscriber(); firehose(emitCount).onBackpressureDrop(new Action1() { @Override From 331075fc2431d109655692f39977792c754f287c Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 18 Mar 2015 09:42:47 -0700 Subject: [PATCH 202/857] Debug build failures --- gradle/buildViaTravis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index d98e5eb603..7971405326 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -6,7 +6,7 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace --debug elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final --stacktrace From 105b47469079ca23183bf48b181d2ead33420390 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 18 Mar 2015 09:47:58 -0700 Subject: [PATCH 203/857] Adding caching to Travis --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 35cfbac86f..1c6f93f983 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,11 @@ sudo: false # script for build and release via Travis to Bintray script: gradle/buildViaTravis.sh +# cache between builds +cache: + directories: + - $HOME/.m2 + # secure environment variables for release to Bintray env: global: From af17baa541f1d092f52009737d688e786339bf96 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 18 Mar 2015 09:55:48 -0700 Subject: [PATCH 204/857] memory for test Attempt at fixing build failures https://github.com/ReactiveX/RxJava/issues/2832 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index d07776fb1d..aa71ebc388 100644 --- a/build.gradle +++ b/build.gradle @@ -27,3 +27,6 @@ if (project.hasProperty('release.useLastTag')) { tasks.prepare.enabled = false } +test{ + maxHeapSize = "1g" +} From 675c07056e7065a6b6fa7ce1226c25cd053ebee2 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 18 Mar 2015 09:56:19 -0700 Subject: [PATCH 205/857] Removing debug --- gradle/buildViaTravis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/buildViaTravis.sh b/gradle/buildViaTravis.sh index 7971405326..d98e5eb603 100755 --- a/gradle/buildViaTravis.sh +++ b/gradle/buildViaTravis.sh @@ -6,7 +6,7 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then ./gradlew -Prelease.useLastTag=true build elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' - ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace --debug + ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final --stacktrace From 75b9ec653f575437180be9fd00e4685b979fb1ad Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 18 Mar 2015 09:58:22 -0700 Subject: [PATCH 206/857] Add .gradle to cache --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1c6f93f983..8099e7d4bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ script: gradle/buildViaTravis.sh cache: directories: - $HOME/.m2 + - $HOME/.gradle # secure environment variables for release to Bintray env: From f260a574127846e66e2385f862f6334103e97982 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 18 Mar 2015 10:16:51 -0700 Subject: [PATCH 207/857] increase memory for test --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index aa71ebc388..412ccb14d2 100644 --- a/build.gradle +++ b/build.gradle @@ -28,5 +28,5 @@ if (project.hasProperty('release.useLastTag')) { } test{ - maxHeapSize = "1g" + maxHeapSize = "2g" } From 3b911b88bc62dac78e450251d03b676deff70e44 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 18 Mar 2015 18:26:12 +0100 Subject: [PATCH 208/857] Fixed a non-deterministic test and a few scheduler leaks. --- src/test/java/rx/BackpressureTests.java | 2 +- .../rx/schedulers/AbstractSchedulerTests.java | 72 +++++------ .../java/rx/subjects/BehaviorSubjectTest.java | 117 ++++++++++-------- .../ReplaySubjectBoundedConcurrencyTest.java | 112 +++++++++-------- 4 files changed, 159 insertions(+), 144 deletions(-) diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index 58f3c147a3..ffa2e01129 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -446,7 +446,7 @@ public void testOnBackpressureDropWithAction() { final AtomicInteger emitCount = new AtomicInteger(); final AtomicInteger dropCount = new AtomicInteger(); final AtomicInteger passCount = new AtomicInteger(); - final int NUM = RxRingBuffer.SIZE * 3 / 2; // > 1 so that take doesn't prevent buffer overflow + final int NUM = RxRingBuffer.SIZE * 3; // > 1 so that take doesn't prevent buffer overflow TestSubscriber ts = new TestSubscriber(); firehose(emitCount).onBackpressureDrop(new Action1() { @Override diff --git a/src/test/java/rx/schedulers/AbstractSchedulerTests.java b/src/test/java/rx/schedulers/AbstractSchedulerTests.java index 53f77abcc6..f27c43807c 100644 --- a/src/test/java/rx/schedulers/AbstractSchedulerTests.java +++ b/src/test/java/rx/schedulers/AbstractSchedulerTests.java @@ -154,43 +154,45 @@ public String call(String s) { public final void testSequenceOfActions() throws InterruptedException { final Scheduler scheduler = getScheduler(); final Scheduler.Worker inner = scheduler.createWorker(); - - final CountDownLatch latch = new CountDownLatch(2); - final Action0 first = mock(Action0.class); - final Action0 second = mock(Action0.class); - - // make it wait until both the first and second are called - doAnswer(new Answer() { - - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - try { - return invocation.getMock(); - } finally { - latch.countDown(); + try { + final CountDownLatch latch = new CountDownLatch(2); + final Action0 first = mock(Action0.class); + final Action0 second = mock(Action0.class); + + // make it wait until both the first and second are called + doAnswer(new Answer() { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + try { + return invocation.getMock(); + } finally { + latch.countDown(); + } } - } - }).when(first).call(); - doAnswer(new Answer() { - - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - try { - return invocation.getMock(); - } finally { - latch.countDown(); + }).when(first).call(); + doAnswer(new Answer() { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + try { + return invocation.getMock(); + } finally { + latch.countDown(); + } } - } - }).when(second).call(); - - inner.schedule(first); - inner.schedule(second); - - latch.await(); - - verify(first, times(1)).call(); - verify(second, times(1)).call(); - + }).when(second).call(); + + inner.schedule(first); + inner.schedule(second); + + latch.await(); + + verify(first, times(1)).call(); + verify(second, times(1)).call(); + } finally { + inner.unsubscribe(); + } } @Test diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index 0036ca5d7a..c64afa4efb 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -426,63 +426,72 @@ public void testOnErrorThrowsDoesntPreventDelivery2() { public void testEmissionSubscriptionRace() throws Exception { Scheduler s = Schedulers.io(); Scheduler.Worker worker = Schedulers.io().createWorker(); - for (int i = 0; i < 50000; i++) { - if (i % 1000 == 0) { - System.out.println(i); - } - final BehaviorSubject rs = BehaviorSubject.create(); - - final CountDownLatch finish = new CountDownLatch(1); - final CountDownLatch start = new CountDownLatch(1); - - worker.schedule(new Action0() { - @Override - public void call() { - try { - start.await(); - } catch (Exception e1) { - e1.printStackTrace(); - } - rs.onNext(1); - } - }); - - final AtomicReference o = new AtomicReference(); - - rs.subscribeOn(s).observeOn(Schedulers.io()) - .subscribe(new Observer() { - - @Override - public void onCompleted() { - o.set(-1); - finish.countDown(); - } - - @Override - public void onError(Throwable e) { - o.set(e); - finish.countDown(); - } - - @Override - public void onNext(Object t) { - o.set(t); - finish.countDown(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); } + final BehaviorSubject rs = BehaviorSubject.create(); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Action0() { + @Override + public void call() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference o = new AtomicReference(); - }); - start.countDown(); - - if (!finish.await(5, TimeUnit.SECONDS)) { - System.out.println(o.get()); - System.out.println(rs.hasObservers()); - rs.onCompleted(); - Assert.fail("Timeout @ " + i); - break; - } else { - Assert.assertEquals(1, o.get()); - rs.onCompleted(); + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new Observer() { + + @Override + public void onCompleted() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onCompleted(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Action0() { + @Override + public void call() { + rs.onCompleted(); + } + }); + } } + } finally { + worker.unsubscribe(); } } diff --git a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java index 5c326cdd92..2f7461d2d5 100644 --- a/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectBoundedConcurrencyTest.java @@ -342,68 +342,72 @@ public void run() { public void testReplaySubjectEmissionSubscriptionRace() throws Exception { Scheduler s = Schedulers.io(); Scheduler.Worker worker = Schedulers.io().createWorker(); - for (int i = 0; i < 50000; i++) { - if (i % 1000 == 0) { - System.out.println(i); - } - final ReplaySubject rs = ReplaySubject.createWithSize(2); - - final CountDownLatch finish = new CountDownLatch(1); - final CountDownLatch start = new CountDownLatch(1); - - worker.schedule(new Action0() { - @Override - public void call() { - try { - start.await(); - } catch (Exception e1) { - e1.printStackTrace(); - } - rs.onNext(1); - } - }); - - final AtomicReference o = new AtomicReference(); - - rs.subscribeOn(s).observeOn(Schedulers.io()) - .subscribe(new Observer() { - - @Override - public void onCompleted() { - o.set(-1); - finish.countDown(); - } - - @Override - public void onError(Throwable e) { - o.set(e); - finish.countDown(); - } - - @Override - public void onNext(Object t) { - o.set(t); - finish.countDown(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); } + final ReplaySubject rs = ReplaySubject.createWithSize(2); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); - }); - start.countDown(); - - if (!finish.await(5, TimeUnit.SECONDS)) { - System.out.println(o.get()); - System.out.println(rs.hasObservers()); - rs.onCompleted(); - Assert.fail("Timeout @ " + i); - break; - } else { - Assert.assertEquals(1, o.get()); worker.schedule(new Action0() { @Override public void call() { - rs.onCompleted(); + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference o = new AtomicReference(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new Observer() { + + @Override + public void onCompleted() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onCompleted(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Action0() { + @Override + public void call() { + rs.onCompleted(); + } + }); + } } + } finally { + worker.unsubscribe(); } } @Test(timeout = 5000) From 489236db6c8257deb285267938739c9d75a377f7 Mon Sep 17 00:00:00 2001 From: Roman Romanchuk Date: Sun, 15 Mar 2015 19:37:22 +0100 Subject: [PATCH 209/857] Fixed javadoc for Observable.repeat() method --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7ed81fea06..8daba744db 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5610,7 +5610,7 @@ public final Observable reduce(R initialValue, Func2 acc * * * @return an Observable that emits the items emitted by the source Observable repeatedly and in sequence - * @see ReactiveX operators documentation: Repeahttp://reactivex.io/documentation/operators/create.htmlt + * @see ReactiveX operators documentation: Repeat */ public final Observable repeat() { return OnSubscribeRedo.repeat(this); From a1a868f9bf1b492f2abfdae96aa3daf952a3e934 Mon Sep 17 00:00:00 2001 From: Avram Lyon Date: Thu, 19 Mar 2015 00:31:48 -0700 Subject: [PATCH 210/857] Fix typo in OnSubscribe interface's Javadoc --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 8daba744db..c021331ff1 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -96,7 +96,7 @@ public final static Observable create(OnSubscribe f) { } /** - * Invoked when Obserable.subscribe is called. + * Invoked when Observable.subscribe is called. */ public interface OnSubscribe extends Action1> { // cover for generics insanity From bbd4c3782f9af25e03673a9f7ee0168f1cba4205 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 24 Mar 2015 16:38:28 +0100 Subject: [PATCH 211/857] Fix for issue 2844: wrong target of request on repeat --- .../internal/operators/OnSubscribeRedo.java | 2 ++ .../operators/OperatorRepeatTest.java | 27 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 65fcb3eb92..00553eebb9 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -213,12 +213,14 @@ public void call() { Subscriber terminalDelegatingSubscriber = new Subscriber() { @Override public void onCompleted() { + currentProducer.set(null); unsubscribe(); terminals.onNext(Notification.createOnCompleted()); } @Override public void onError(Throwable e) { + currentProducer.set(null); unsubscribe(); terminals.onNext(Notification.createOnError(e)); } diff --git a/src/test/java/rx/internal/operators/OperatorRepeatTest.java b/src/test/java/rx/internal/operators/OperatorRepeatTest.java index d8653a14e6..44371867c5 100644 --- a/src/test/java/rx/internal/operators/OperatorRepeatTest.java +++ b/src/test/java/rx/internal/operators/OperatorRepeatTest.java @@ -20,7 +20,8 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; -import java.util.Arrays; +import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; @@ -174,4 +175,28 @@ public void testRepeatAndDistinctUnbounded() { ts.assertTerminalEvent(); ts.assertReceivedOnNext(Arrays.asList(1, 2, 3)); } + /** Issue #2844: wrong target of request. */ + @Test(timeout = 3000) + public void testRepeatRetarget() { + final List concatBase = new ArrayList(); + TestSubscriber ts = new TestSubscriber(); + Observable.just(1, 2) + .repeat(5) + .concatMap(new Func1>() { + @Override + public Observable call(Integer x) { + System.out.println("testRepeatRetarget -> " + x); + concatBase.add(x); + return Observable.empty() + .delay(200, TimeUnit.MILLISECONDS); + } + }) + .subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.emptyList()); + + assertEquals(Arrays.asList(1, 2, 1, 2, 1, 2, 1, 2, 1, 2), concatBase); + } } From afe2b41de1f4c95a6cb39c1fb3c1de0273911493 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Wed, 1 Apr 2015 17:53:02 +0800 Subject: [PATCH 212/857] Add 'request(Long.MAX_VALUE)' in 'onStart' to fix the backpressure issue of debounce --- .../rx/internal/operators/OperatorDebounceWithTime.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java index b97bc90d3e..d8d0089441 100644 --- a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java @@ -62,6 +62,12 @@ public Subscriber call(final Subscriber child) { return new Subscriber(child) { final DebounceState state = new DebounceState(); final Subscriber self = this; + + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + @Override public void onNext(final T t) { From 8b6035208b8995a8d246b8bc743b9e86a70d88cd Mon Sep 17 00:00:00 2001 From: zsxwing Date: Wed, 1 Apr 2015 20:18:11 +0800 Subject: [PATCH 213/857] Add a unit test for debounce's backpressure issue --- .../operators/OperatorDebounceTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/rx/internal/operators/OperatorDebounceTest.java b/src/test/java/rx/internal/operators/OperatorDebounceTest.java index 174820f310..e43efa2f7f 100644 --- a/src/test/java/rx/internal/operators/OperatorDebounceTest.java +++ b/src/test/java/rx/internal/operators/OperatorDebounceTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.Arrays; import java.util.concurrent.TimeUnit; import org.junit.Before; @@ -36,6 +37,7 @@ import rx.exceptions.TestException; import rx.functions.Action0; import rx.functions.Func1; +import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; @@ -287,4 +289,20 @@ public Observable call(Integer t1) { verify(o).onCompleted(); verify(o, never()).onError(any(Throwable.class)); } + + @Test + public void debounceWithTimeBackpressure() throws InterruptedException { + TestScheduler scheduler = new TestScheduler(); + TestSubscriber subscriber = new TestSubscriber(); + Observable.merge( + Observable.just(1), + Observable.just(2).delay(10, TimeUnit.MILLISECONDS, scheduler) + ).debounce(20, TimeUnit.MILLISECONDS, scheduler).take(1).subscribe(subscriber); + + scheduler.advanceTimeBy(30, TimeUnit.MILLISECONDS); + + subscriber.assertReceivedOnNext(Arrays.asList(2)); + subscriber.assertTerminalEvent(); + subscriber.assertNoErrors(); + } } \ No newline at end of file From 0a6e26de73c3707dbd9903c166b7be0f6499b6b7 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 2 Apr 2015 08:51:09 +0200 Subject: [PATCH 214/857] Change retryWhen to eagerly ignore an error'd source's subsequent events --- .../internal/operators/OnSubscribeRedo.java | 27 ++++++---- .../operators/OperatorRetryWithPredicate.java | 26 +++++---- .../OperatorRetryWithPredicateTest.java | 53 +++++++++++++++---- 3 files changed, 79 insertions(+), 27 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 00553eebb9..1ba5d1f281 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -211,26 +211,35 @@ public void call() { } Subscriber terminalDelegatingSubscriber = new Subscriber() { + boolean done; @Override public void onCompleted() { - currentProducer.set(null); - unsubscribe(); - terminals.onNext(Notification.createOnCompleted()); + if (!done) { + done = true; + currentProducer.set(null); + unsubscribe(); + terminals.onNext(Notification.createOnCompleted()); + } } @Override public void onError(Throwable e) { - currentProducer.set(null); - unsubscribe(); - terminals.onNext(Notification.createOnError(e)); + if (!done) { + done = true; + currentProducer.set(null); + unsubscribe(); + terminals.onNext(Notification.createOnError(e)); + } } @Override public void onNext(T v) { - if (consumerCapacity.get() != Long.MAX_VALUE) { - consumerCapacity.decrementAndGet(); + if (!done) { + if (consumerCapacity.get() != Long.MAX_VALUE) { + consumerCapacity.decrementAndGet(); + } + child.onNext(v); } - child.onNext(v); } @Override diff --git a/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java b/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java index 92eb34ca0f..24beeec2a0 100644 --- a/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java @@ -84,26 +84,34 @@ public void call() { // new subscription each time so if it unsubscribes itself it does not prevent retries // by unsubscribing the child subscription Subscriber subscriber = new Subscriber() { - + boolean done; @Override public void onCompleted() { - child.onCompleted(); + if (!done) { + done = true; + child.onCompleted(); + } } @Override public void onError(Throwable e) { - if (predicate.call(attempts, e) && !inner.isUnsubscribed()) { - // retry again - inner.schedule(_self); - } else { - // give up and pass the failure - child.onError(e); + if (!done) { + done = true; + if (predicate.call(attempts, e) && !inner.isUnsubscribed()) { + // retry again + inner.schedule(_self); + } else { + // give up and pass the failure + child.onError(e); + } } } @Override public void onNext(T v) { - child.onNext(v); + if (!done) { + child.onNext(v); + } } }; diff --git a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java index 008085dd01..ee4750829a 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java @@ -15,21 +15,23 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + import java.io.IOException; +import java.util.Collections; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; +import java.util.concurrent.atomic.*; + import org.junit.Test; import org.mockito.InOrder; -import static org.mockito.Mockito.*; -import rx.Observable; + +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Subscriber; -import rx.Subscription; import rx.exceptions.TestException; -import rx.functions.Action1; -import rx.functions.Func2; +import rx.functions.*; +import rx.observers.TestSubscriber; import rx.subjects.PublishSubject; public class OperatorRetryWithPredicateTest { @@ -270,4 +272,37 @@ public void testTimeoutWithRetry() { assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); } + + @Test + public void testIssue2826() { + TestSubscriber ts = new TestSubscriber(); + final RuntimeException e = new RuntimeException("You shall not pass"); + final AtomicInteger c = new AtomicInteger(); + Observable.just(1).map(new Func1() { + @Override + public Integer call(Integer t1) { + c.incrementAndGet(); + throw e; + } + }).retry(retry5).subscribe(ts); + + ts.assertTerminalEvent(); + assertEquals(6, c.get()); + assertEquals(Collections.singletonList(e), ts.getOnErrorEvents()); + } + @Test + public void testJustAndRetry() throws Exception { + final AtomicBoolean throwException = new AtomicBoolean(true); + int value = Observable.just(1).map(new Func1() { + @Override + public Integer call(Integer t1) { + if (throwException.compareAndSet(true, false)) { + throw new TestException(); + } + return t1; + } + }).retry(1).toBlocking().single(); + + assertEquals(1, value); + } } From d7363870233114dc6b2b6a765e43b22265ca4c43 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 7 Apr 2015 15:28:09 +1000 Subject: [PATCH 215/857] fix unsub of OperatorDoOnRequest and add more unit tests --- .../operators/OperatorDoOnRequest.java | 2 +- .../operators/OperatorDoOnRequestTest.java | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java diff --git a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java index d77cc21dba..2c77a584ca 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java @@ -48,7 +48,7 @@ public void request(long n) { } }); - + child.add(parent); return parent; } diff --git a/src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java b/src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java new file mode 100644 index 0000000000..34014094b6 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java @@ -0,0 +1,80 @@ +package rx.internal.operators; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; + +public class OperatorDoOnRequestTest { + + @Test + public void testUnsubscribeHappensAgainstParent() { + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Observable.just(1) + // + .doOnUnsubscribe(new Action0() { + @Override + public void call() { + unsubscribed.set(true); + } + }) + // + .doOnRequest(new Action1() { + @Override + public void call(Long n) { + // do nothing + } + }) + // + .subscribe(); + assertTrue(unsubscribed.get()); + } + + @Test + public void testDoRequest() { + final List requests = new ArrayList(); + Observable.range(1, 5) + // + .doOnRequest(new Action1() { + @Override + public void call(Long n) { + requests.add(n); + } + }) + // + .subscribe(new Subscriber() { + + @Override + public void onStart() { + request(3); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(t); + } + }); + assertEquals(Arrays.asList(3L,1L,2L,3L,4L,5L), requests); + } + +} From 31339a26e6069e39f8c64fd68cae0260119e6071 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 7 Apr 2015 11:44:47 +0200 Subject: [PATCH 216/857] Fixed request accounting, increased visibility of BackpressureUtils --- .../internal/operators/BackpressureUtils.java | 23 +++++++----- .../rx/observables/AbstractOnSubscribe.java | 16 +++++---- .../observables/AbstractOnSubscribeTest.java | 36 ++++++++++++++++++- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index 7e7ce2ae55..c62eefcbbc 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -22,8 +22,11 @@ * Utility functions for use with backpressure. * */ -final class BackpressureUtils { - +public final class BackpressureUtils { + /** Utility class, no instances. */ + private BackpressureUtils() { + throw new IllegalStateException("No instances!"); + } /** * Adds {@code n} to {@code requested} field and returns the value prior to * addition once the addition is successful (uses CAS semantics). If @@ -37,16 +40,18 @@ final class BackpressureUtils { * the number of requests to add to the requested count * @return requested value just prior to successful addition */ - static long getAndAddRequest(AtomicLongFieldUpdater requested, T object, long n) { + public static long getAndAddRequest(AtomicLongFieldUpdater requested, T object, long n) { // add n to field but check for overflow while (true) { long current = requested.get(object); long next = current + n; // check for overflow - if (next < 0) + if (next < 0) { next = Long.MAX_VALUE; - if (requested.compareAndSet(object, current, next)) + } + if (requested.compareAndSet(object, current, next)) { return current; + } } } @@ -63,16 +68,18 @@ static long getAndAddRequest(AtomicLongFieldUpdater requested, T object, * the number of requests to add to the requested count * @return requested value just prior to successful addition */ - static long getAndAddRequest(AtomicLong requested, long n) { + public static long getAndAddRequest(AtomicLong requested, long n) { // add n to field but check for overflow while (true) { long current = requested.get(); long next = current + n; // check for overflow - if (next < 0) + if (next < 0) { next = Long.MAX_VALUE; - if (requested.compareAndSet(current, next)) + } + if (requested.compareAndSet(current, next)) { return current; + } } } } diff --git a/src/main/java/rx/observables/AbstractOnSubscribe.java b/src/main/java/rx/observables/AbstractOnSubscribe.java index ea78c56c75..1a1526766e 100644 --- a/src/main/java/rx/observables/AbstractOnSubscribe.java +++ b/src/main/java/rx/observables/AbstractOnSubscribe.java @@ -24,6 +24,7 @@ import rx.annotations.Experimental; import rx.exceptions.CompositeException; import rx.functions.*; +import rx.internal.operators.BackpressureUtils; /** * Abstract base class for the {@link OnSubscribe} interface that helps you build Observable sources one @@ -332,14 +333,15 @@ private SubscriptionProducer(SubscriptionState state) { } @Override public void request(long n) { - if (n == Long.MAX_VALUE) { - for (; !state.subscriber.isUnsubscribed(); ) { - if (!doNext()) { - break; + if (n > 0 && BackpressureUtils.getAndAddRequest(state.requestCount, n) == 0) { + if (n == Long.MAX_VALUE) { + // fast-path + for (; !state.subscriber.isUnsubscribed(); ) { + if (!doNext()) { + break; + } } - } - } else - if (n > 0 && state.requestCount.getAndAdd(n) == 0) { + } else if (!state.subscriber.isUnsubscribed()) { do { if (!doNext()) { diff --git a/src/test/java/rx/observables/AbstractOnSubscribeTest.java b/src/test/java/rx/observables/AbstractOnSubscribeTest.java index e408a166f0..95e3eac011 100644 --- a/src/test/java/rx/observables/AbstractOnSubscribeTest.java +++ b/src/test/java/rx/observables/AbstractOnSubscribeTest.java @@ -16,12 +16,13 @@ package rx.observables; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import org.mockito.InOrder; @@ -503,4 +504,37 @@ public void testMissingEmission() { verify(o, never()).onNext(any(Object.class)); verify(o).onError(any(IllegalStateException.class)); } + + @Test + public void testCanRequestInOnNext() { + AbstractOnSubscribe aos = new AbstractOnSubscribe() { + @Override + protected void next(SubscriptionState state) { + state.onNext(1); + state.onCompleted(); + } + }; + final AtomicReference exception = new AtomicReference(); + aos.toObservable().subscribe(new Subscriber() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + exception.set(e); + } + + @Override + public void onNext(Integer t) { + request(1); + } + }); + if (exception.get()!=null) { + exception.get().printStackTrace(); + } + assertNull(exception.get()); + } } From d51763dda212094c841f971c7716e596a15f3e8f Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 6 Apr 2015 12:52:53 +1000 Subject: [PATCH 217/857] OperatorSingle should request exactly what it needs --- .../rx/internal/operators/OperatorSingle.java | 101 +++++++---- .../operators/OperatorSingleTest.java | 170 +++++++++++++++++- 2 files changed, 235 insertions(+), 36 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSingle.java b/src/main/java/rx/internal/operators/OperatorSingle.java index 5b3ea507cb..7a400dd9be 100644 --- a/src/main/java/rx/internal/operators/OperatorSingle.java +++ b/src/main/java/rx/internal/operators/OperatorSingle.java @@ -16,8 +16,10 @@ package rx.internal.operators; import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicBoolean; import rx.Observable.Operator; +import rx.Producer; import rx.Subscriber; /** @@ -44,53 +46,84 @@ private OperatorSingle(boolean hasDefaultValue, final T defaultValue) { } @Override - public Subscriber call(final Subscriber subscriber) { - return new Subscriber(subscriber) { + public Subscriber call(final Subscriber child) { - private T value; - private boolean isNonEmpty = false; - private boolean hasTooManyElements = false; + final ParentSubscriber parent = new ParentSubscriber(child, hasDefaultValue, + defaultValue); + + child.setProducer(new Producer() { + + private final AtomicBoolean requestedTwo = new AtomicBoolean(false); @Override - public void onNext(T value) { - if (isNonEmpty) { - hasTooManyElements = true; - subscriber.onError(new IllegalArgumentException("Sequence contains too many elements")); - unsubscribe(); - } else { - this.value = value; - isNonEmpty = true; - // Issue: https://github.com/ReactiveX/RxJava/pull/1527 - // Because we cache a value and don't emit now, we need to request another one. - request(1); + public void request(long n) { + if (n > 0 && requestedTwo.compareAndSet(false, true)) { + parent.requestMore(2); } } - @Override - public void onCompleted() { - if (hasTooManyElements) { - // We have already sent an onError message + }); + child.add(parent); + return parent; + } + + private static final class ParentSubscriber extends Subscriber { + private final Subscriber child; + private final boolean hasDefaultValue; + private final T defaultValue; + + private T value; + private boolean isNonEmpty = false; + private boolean hasTooManyElements = false; + + + ParentSubscriber(Subscriber child, boolean hasDefaultValue, + T defaultValue) { + this.child = child; + this.hasDefaultValue = hasDefaultValue; + this.defaultValue = defaultValue; + } + + void requestMore(long n) { + request(n); + } + + @Override + public void onNext(T value) { + if (isNonEmpty) { + hasTooManyElements = true; + child.onError(new IllegalArgumentException("Sequence contains too many elements")); + unsubscribe(); + } else { + this.value = value; + isNonEmpty = true; + } + } + + @Override + public void onCompleted() { + if (hasTooManyElements) { + // We have already sent an onError message + } else { + if (isNonEmpty) { + child.onNext(value); + child.onCompleted(); } else { - if (isNonEmpty) { - subscriber.onNext(value); - subscriber.onCompleted(); + if (hasDefaultValue) { + child.onNext(defaultValue); + child.onCompleted(); } else { - if (hasDefaultValue) { - subscriber.onNext(defaultValue); - subscriber.onCompleted(); - } else { - subscriber.onError(new NoSuchElementException("Sequence contains no elements")); - } + child.onError(new NoSuchElementException("Sequence contains no elements")); } } } + } - @Override - public void onError(Throwable e) { - subscriber.onError(e); - } + @Override + public void onError(Throwable e) { + child.onError(e); + } - }; } } diff --git a/src/test/java/rx/internal/operators/OperatorSingleTest.java b/src/test/java/rx/internal/operators/OperatorSingleTest.java index bf619f9e6a..f1490e8910 100644 --- a/src/test/java/rx/internal/operators/OperatorSingleTest.java +++ b/src/test/java/rx/internal/operators/OperatorSingleTest.java @@ -17,9 +17,16 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.isA; -import static org.mockito.Mockito.*; - +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicLong; import org.junit.Test; import org.mockito.InOrder; @@ -27,6 +34,7 @@ import rx.Observable; import rx.Observer; import rx.Subscriber; +import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; @@ -73,6 +81,164 @@ public void testSingleWithEmpty() { isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } + + @Test + public void testSingleDoesNotRequestMoreThanItNeedsToEmitItem() { + final AtomicLong request = new AtomicLong(); + Observable.just(1).doOnRequest(new Action1() { + @Override + public void call(Long n) { + request.addAndGet(n); + } + }).toBlocking().single(); + assertEquals(2, request.get()); + } + + @Test + public void testSingleDoesNotRequestMoreThanItNeedsToEmitErrorFromEmpty() { + final AtomicLong request = new AtomicLong(); + try { + Observable.empty().doOnRequest(new Action1() { + @Override + public void call(Long n) { + request.addAndGet(n); + } + }).toBlocking().single(); + } catch (NoSuchElementException e) { + assertEquals(2, request.get()); + } + } + + @Test + public void testSingleDoesNotRequestMoreThanItNeedsToEmitErrorFromMoreThanOne() { + final AtomicLong request = new AtomicLong(); + try { + Observable.just(1, 2).doOnRequest(new Action1() { + @Override + public void call(Long n) { + request.addAndGet(n); + } + }).toBlocking().single(); + } catch (IllegalArgumentException e) { + assertEquals(2, request.get()); + } + } + + @Test + public void testSingleDoesNotRequestMoreThanItNeedsIf1Then2Requested() { + final List requests = new ArrayList(); + Observable.just(1) + // + .doOnRequest(new Action1() { + @Override + public void call(Long n) { + requests.add(n); + } + }) + // + .single() + // + .subscribe(new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(2); + } + }); + assertEquals(Arrays.asList(2L), requests); + } + + @Test + public void testSingleDoesNotRequestMoreThanItNeedsIf3Requested() { + final List requests = new ArrayList(); + Observable.just(1) + // + .doOnRequest(new Action1() { + @Override + public void call(Long n) { + requests.add(n); + } + }) + // + .single() + // + .subscribe(new Subscriber() { + + @Override + public void onStart() { + request(3); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + } + }); + assertEquals(Arrays.asList(2L), requests); + } + + @Test + public void testSingleRequestsExactlyWhatItNeedsIf1Requested() { + final List requests = new ArrayList(); + Observable.just(1) + // + .doOnRequest(new Action1() { + @Override + public void call(Long n) { + requests.add(n); + } + }) + // + .single() + // + .subscribe(new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + } + }); + assertEquals(Arrays.asList(2L), requests); + } + @Test public void testSingleWithPredicate() { From 2f7145a68547416f100b2351405748424799fac9 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 8 Apr 2015 09:37:09 +1000 Subject: [PATCH 218/857] ensure OperatorZipTest.testZipRace does not fail on slower machines --- src/test/java/rx/internal/operators/OperatorZipTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/java/rx/internal/operators/OperatorZipTest.java b/src/test/java/rx/internal/operators/OperatorZipTest.java index 8c7d453e47..d9487c5b03 100644 --- a/src/test/java/rx/internal/operators/OperatorZipTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipTest.java @@ -1270,8 +1270,14 @@ public void onNext(Integer t) { } @Test(timeout = 10000) public void testZipRace() { + long startTime = System.currentTimeMillis(); Observable src = Observable.just(1).subscribeOn(Schedulers.computation()); - for (int i = 0; i < 100000; i++) { + + // now try and generate a hang by zipping src with itself repeatedly. A + // time limit of 9 seconds ( 1 second less than the test timeout) is + // used so that this test will not timeout on slow machines. + int i = 0; + while (System.currentTimeMillis()-startTime < 9000 && i++ < 100000) { int value = Observable.zip(src, src, new Func2() { @Override public Integer call(Integer t1, Integer t2) { From 5d513f23dfb228e49ea2fcc06e6d063370a844c2 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 8 Apr 2015 10:07:15 +1000 Subject: [PATCH 219/857] warnings cleanup in test source --- src/test/java/rx/CovarianceTest.java | 6 ++- src/test/java/rx/ObservableTests.java | 6 +-- .../OnSubscribeToObservableFutureTest.java | 2 +- .../operators/OperatorConcatTest.java | 4 +- .../operators/OperatorDoOnEachTest.java | 12 ++--- .../operators/OperatorMulticastTest.java | 1 - ...torOnErrorResumeNextViaObservableTest.java | 4 -- .../operators/OperatorPublishTest.java | 2 +- .../operators/OperatorReplayTest.java | 45 +++++++++++-------- .../operators/OperatorSampleTest.java | 1 - .../internal/operators/OperatorScanTest.java | 2 +- .../operators/OperatorSkipWhileTest.java | 1 - .../internal/util/IndexedRingBufferTest.java | 2 +- 13 files changed, 47 insertions(+), 41 deletions(-) diff --git a/src/test/java/rx/CovarianceTest.java b/src/test/java/rx/CovarianceTest.java index 8b56fa0c49..6c60d8a73f 100644 --- a/src/test/java/rx/CovarianceTest.java +++ b/src/test/java/rx/CovarianceTest.java @@ -106,6 +106,7 @@ public String call(Movie m) { assertEquals(6, ts.getOnNextEvents().size()); } + @SuppressWarnings("unused") @Test public void testCovarianceOfCompose() { Observable movie = Observable.just(new HorrorMovie()); @@ -119,6 +120,7 @@ public Observable call(Observable t1) { }); } + @SuppressWarnings("unused") @Test public void testCovarianceOfCompose2() { Observable movie = Observable. just(new HorrorMovie()); @@ -130,6 +132,7 @@ public Observable call(Observable t1) { }); } + @SuppressWarnings("unused") @Test public void testCovarianceOfCompose3() { Observable movie = Observable.just(new HorrorMovie()); @@ -147,6 +150,7 @@ public HorrorMovie call(HorrorMovie horrorMovie) { }); } + @SuppressWarnings("unused") @Test public void testCovarianceOfCompose4() { Observable movie = Observable.just(new HorrorMovie()); @@ -201,7 +205,7 @@ public Observable call(List> listOfLists) { oldList.removeAll(newList); // for all left in the oldList we'll create DROP events - for (Movie old : oldList) { + for (@SuppressWarnings("unused") Movie old : oldList) { delta.add(new Movie()); } diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index e5ed491da1..badbfe262e 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -1054,9 +1054,9 @@ public Observable call(String s) { @Test public void testTakeWhileToList() { - int[] nums = {1, 2, 3}; + final int expectedCount = 3; final AtomicInteger count = new AtomicInteger(); - for(final int n: nums) { + for (int i = 0;i < expectedCount; i++) { Observable .just(Boolean.TRUE, Boolean.FALSE) .takeWhile(new Func1() { @@ -1074,7 +1074,7 @@ public void call(List booleans) { }) .subscribe(); } - assertEquals(nums.length, count.get()); + assertEquals(expectedCount, count.get()); } @Test diff --git a/src/test/java/rx/internal/operators/OnSubscribeToObservableFutureTest.java b/src/test/java/rx/internal/operators/OnSubscribeToObservableFutureTest.java index 55b34b38b1..ff37d8fab3 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeToObservableFutureTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeToObservableFutureTest.java @@ -88,7 +88,7 @@ public void testCancelledBeforeSubscribe() throws Exception { TestSubscriber testSubscriber = new TestSubscriber(o); testSubscriber.unsubscribe(); - Subscription sub = Observable.from(future).subscribe(testSubscriber); + Observable.from(future).subscribe(testSubscriber); assertEquals(0, testSubscriber.getOnErrorEvents().size()); assertEquals(0, testSubscriber.getOnCompletedEvents().size()); } diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 53b6f320a9..688b0331f2 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -485,11 +485,11 @@ public boolean isUnsubscribed() { private final T seed; private final int size; - public TestObservable(T... values) { + public TestObservable(@SuppressWarnings("unchecked") T... values) { this(null, null, values); } - public TestObservable(CountDownLatch once, CountDownLatch okToContinue, T... values) { + public TestObservable(CountDownLatch once, CountDownLatch okToContinue, @SuppressWarnings("unchecked") T... values) { this.values = Arrays.asList(values); this.size = this.values.size(); this.once = once; diff --git a/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java b/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java index ea3c90c51f..2ad9a36828 100644 --- a/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java +++ b/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java @@ -123,9 +123,9 @@ public void call(String s) { @Test public void testIssue1451Case1() { // https://github.com/Netflix/RxJava/issues/1451 - int[] nums = { 1, 2, 3 }; + final int expectedCount = 3; final AtomicInteger count = new AtomicInteger(); - for (final int n : nums) { + for (int i=0; i < expectedCount; i++) { Observable .just(Boolean.TRUE, Boolean.FALSE) .takeWhile(new Func1() { @@ -143,15 +143,15 @@ public void call(List booleans) { }) .subscribe(); } - assertEquals(nums.length, count.get()); + assertEquals(expectedCount, count.get()); } @Test public void testIssue1451Case2() { // https://github.com/Netflix/RxJava/issues/1451 - int[] nums = { 1, 2, 3 }; + final int expectedCount = 3; final AtomicInteger count = new AtomicInteger(); - for (final int n : nums) { + for (int i=0; i < expectedCount; i++) { Observable .just(Boolean.TRUE, Boolean.FALSE, Boolean.FALSE) .takeWhile(new Func1() { @@ -169,7 +169,7 @@ public void call(List booleans) { }) .subscribe(); } - assertEquals(nums.length, count.get()); + assertEquals(expectedCount, count.get()); } @Test diff --git a/src/test/java/rx/internal/operators/OperatorMulticastTest.java b/src/test/java/rx/internal/operators/OperatorMulticastTest.java index 5b3c57e4f6..bbde4a3751 100644 --- a/src/test/java/rx/internal/operators/OperatorMulticastTest.java +++ b/src/test/java/rx/internal/operators/OperatorMulticastTest.java @@ -21,7 +21,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import org.junit.Assert; import org.junit.Test; import rx.Observer; diff --git a/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java b/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java index 39c345e97a..586c2b689d 100644 --- a/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java @@ -21,8 +21,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.concurrent.TimeUnit; - import org.junit.Test; import org.mockito.Mockito; @@ -109,7 +107,6 @@ public String call(String s) { @Test public void testResumeNextWithFailedOnSubscribe() { - Subscription s = mock(Subscription.class); Observable testObservable = Observable.create(new OnSubscribe() { @Override @@ -132,7 +129,6 @@ public void call(Subscriber t1) { @Test public void testResumeNextWithFailedOnSubscribeAsync() { - Subscription s = mock(Subscription.class); Observable testObservable = Observable.create(new OnSubscribe() { @Override diff --git a/src/test/java/rx/internal/operators/OperatorPublishTest.java b/src/test/java/rx/internal/operators/OperatorPublishTest.java index e8dc5312fb..3d8481a676 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishTest.java @@ -251,7 +251,7 @@ public void testConnectWithNoSubscriber() { co.connect(); // Emit 0 scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); - TestSubscriber subscriber = new TestSubscriber(); + TestSubscriber subscriber = new TestSubscriber(); co.subscribe(subscriber); // Emit 1 and 2 scheduler.advanceTimeBy(50, TimeUnit.MILLISECONDS); diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index bd15f03b8b..a5ff85864d 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -15,18 +15,24 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.notNull; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.notNull; -import static org.mockito.Mockito.*; import org.junit.Test; import org.mockito.InOrder; - import rx.Observable; import rx.Observer; import rx.Scheduler; @@ -523,14 +529,15 @@ public void call() { /** * test the basic expectation of OperatorMulticast via replay */ + @SuppressWarnings("unchecked") @Test public void testIssue2191_UnsubscribeSource() { // setup mocks - Action1 sourceNext = mock(Action1.class); + Action1 sourceNext = mock(Action1.class); Action0 sourceCompleted = mock(Action0.class); Action0 sourceUnsubscribed = mock(Action0.class); - Observer spiedSubscriberBeforeConnect = mock(Observer.class); - Observer spiedSubscriberAfterConnect = mock(Observer.class); + Observer spiedSubscriberBeforeConnect = mock(Observer.class); + Observer spiedSubscriberAfterConnect = mock(Observer.class); // Observable under test Observable source = Observable.just(1,2); @@ -570,17 +577,18 @@ public void testIssue2191_UnsubscribeSource() { * * @throws Exception */ + @SuppressWarnings("unchecked") @Test public void testIssue2191_SchedulerUnsubscribe() throws Exception { // setup mocks - Action1 sourceNext = mock(Action1.class); + Action1 sourceNext = mock(Action1.class); Action0 sourceCompleted = mock(Action0.class); Action0 sourceUnsubscribed = mock(Action0.class); final Scheduler mockScheduler = mock(Scheduler.class); final Subscription mockSubscription = mock(Subscription.class); Worker spiedWorker = workerSpy(mockSubscription); - Observer mockObserverBeforeConnect = mock(Observer.class); - Observer mockObserverAfterConnect = mock(Observer.class); + Observer mockObserverBeforeConnect = mock(Observer.class); + Observer mockObserverAfterConnect = mock(Observer.class); when(mockScheduler.createWorker()).thenReturn(spiedWorker); @@ -626,18 +634,19 @@ public void testIssue2191_SchedulerUnsubscribe() throws Exception { * * @throws Exception */ + @SuppressWarnings("unchecked") @Test public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { // setup mocks - Action1 sourceNext = mock(Action1.class); + Action1 sourceNext = mock(Action1.class); Action0 sourceCompleted = mock(Action0.class); - Action1 sourceError = mock(Action1.class); + Action1 sourceError = mock(Action1.class); Action0 sourceUnsubscribed = mock(Action0.class); final Scheduler mockScheduler = mock(Scheduler.class); final Subscription mockSubscription = mock(Subscription.class); Worker spiedWorker = workerSpy(mockSubscription); - Observer mockObserverBeforeConnect = mock(Observer.class); - Observer mockObserverAfterConnect = mock(Observer.class); + Observer mockObserverBeforeConnect = mock(Observer.class); + Observer mockObserverAfterConnect = mock(Observer.class); when(mockScheduler.createWorker()).thenReturn(spiedWorker); @@ -682,14 +691,14 @@ public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { verifyNoMoreInteractions(mockObserverAfterConnect); } - private static void verifyObserverMock(Observer mock, int numSubscriptions, int numItemsExpected) { - verify(mock, times(numItemsExpected)).onNext(notNull()); + private static void verifyObserverMock(Observer mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); verify(mock, times(numSubscriptions)).onCompleted(); verifyNoMoreInteractions(mock); } - private static void verifyObserver(Observer mock, int numSubscriptions, int numItemsExpected, Throwable error) { - verify(mock, times(numItemsExpected)).onNext(notNull()); + private static void verifyObserver(Observer mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); verify(mock, times(numSubscriptions)).onError(error); verifyNoMoreInteractions(mock); } diff --git a/src/test/java/rx/internal/operators/OperatorSampleTest.java b/src/test/java/rx/internal/operators/OperatorSampleTest.java index 815d002061..2ef1ae8fb3 100644 --- a/src/test/java/rx/internal/operators/OperatorSampleTest.java +++ b/src/test/java/rx/internal/operators/OperatorSampleTest.java @@ -33,7 +33,6 @@ import rx.functions.Action0; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; -import rx.subscriptions.Subscriptions; public class OperatorSampleTest { private TestScheduler scheduler; diff --git a/src/test/java/rx/internal/operators/OperatorScanTest.java b/src/test/java/rx/internal/operators/OperatorScanTest.java index 5d2aa0bf1e..e05d4d9bb1 100644 --- a/src/test/java/rx/internal/operators/OperatorScanTest.java +++ b/src/test/java/rx/internal/operators/OperatorScanTest.java @@ -318,7 +318,7 @@ public void testScanShouldNotRequestZero() { final AtomicReference producer = new AtomicReference(); Observable o = Observable.create(new Observable.OnSubscribe() { @Override - public void call(final Subscriber subscriber) { + public void call(final Subscriber subscriber) { Producer p = spy(new Producer() { private AtomicBoolean requested = new AtomicBoolean(false); diff --git a/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java b/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java index 68cd2f1e02..38a93bd5fb 100644 --- a/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java +++ b/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java @@ -29,7 +29,6 @@ import rx.Observable; import rx.Observer; import rx.functions.Func1; -import rx.functions.Func2; public class OperatorSkipWhileTest { diff --git a/src/test/java/rx/internal/util/IndexedRingBufferTest.java b/src/test/java/rx/internal/util/IndexedRingBufferTest.java index 90de1f0c8b..12ccb7afee 100644 --- a/src/test/java/rx/internal/util/IndexedRingBufferTest.java +++ b/src/test/java/rx/internal/util/IndexedRingBufferTest.java @@ -242,7 +242,7 @@ public void longRunningAddRemoveAddDoesntLeakMemory() { list.forEach(newCounterAction(c)); assertEquals(0, c.get()); // System.out.println("Index is: " + list.index.get() + " when it should be no bigger than " + list.SIZE); - assertTrue(list.index.get() < list.SIZE); + assertTrue(list.index.get() < IndexedRingBuffer.SIZE); // it should actually be 1 since we only did add/remove sequentially assertEquals(1, list.index.get()); } From bfad7ea961c9955c4872bb9155dfe60397478d13 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 9 Apr 2015 09:24:28 +1000 Subject: [PATCH 220/857] add generic type to IndexedRingBuffer.POOL and modifiy IndexedRingBuffer.getInstance so that infers type --- .../java/rx/internal/util/IndexedRingBuffer.java | 11 ++++++----- .../internal/util/SubscriptionIndexedRingBuffer.java | 1 - src/perf/java/rx/internal/IndexedRingBufferPerf.java | 2 -- .../java/rx/internal/util/IndexedRingBufferTest.java | 12 +----------- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/main/java/rx/internal/util/IndexedRingBuffer.java b/src/main/java/rx/internal/util/IndexedRingBuffer.java index d353eac65f..f2fe163276 100644 --- a/src/main/java/rx/internal/util/IndexedRingBuffer.java +++ b/src/main/java/rx/internal/util/IndexedRingBuffer.java @@ -48,17 +48,18 @@ */ public final class IndexedRingBuffer implements Subscription { - private static final ObjectPool POOL = new ObjectPool() { + private static final ObjectPool> POOL = new ObjectPool>() { @Override - protected IndexedRingBuffer createObject() { - return new IndexedRingBuffer(); + protected IndexedRingBuffer createObject() { + return new IndexedRingBuffer(); } }; - public final static IndexedRingBuffer getInstance() { - return POOL.borrowObject(); + @SuppressWarnings("unchecked") + public final static IndexedRingBuffer getInstance() { + return (IndexedRingBuffer) POOL.borrowObject(); } private final ElementSection elements = new ElementSection(); diff --git a/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java b/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java index 1b52bc60bb..1c3e0abe3f 100644 --- a/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java +++ b/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java @@ -27,7 +27,6 @@ */ public final class SubscriptionIndexedRingBuffer implements Subscription { - @SuppressWarnings("unchecked") private volatile IndexedRingBuffer subscriptions = IndexedRingBuffer.getInstance(); private volatile int unsubscribed = 0; @SuppressWarnings("rawtypes") diff --git a/src/perf/java/rx/internal/IndexedRingBufferPerf.java b/src/perf/java/rx/internal/IndexedRingBufferPerf.java index bb0a503686..e523e337a6 100644 --- a/src/perf/java/rx/internal/IndexedRingBufferPerf.java +++ b/src/perf/java/rx/internal/IndexedRingBufferPerf.java @@ -29,7 +29,6 @@ public class IndexedRingBufferPerf { @Benchmark public void indexedRingBufferAdd(IndexedRingBufferInput input) throws InterruptedException, MissingBackpressureException { - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); for (int i = 0; i < input.size; i++) { list.add(i); @@ -40,7 +39,6 @@ public void indexedRingBufferAdd(IndexedRingBufferInput input) throws Interrupte @Benchmark public void indexedRingBufferAddRemove(IndexedRingBufferInput input) throws InterruptedException, MissingBackpressureException { - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); for (int i = 0; i < input.size; i++) { list.add(i); diff --git a/src/test/java/rx/internal/util/IndexedRingBufferTest.java b/src/test/java/rx/internal/util/IndexedRingBufferTest.java index 90de1f0c8b..d0472583c9 100644 --- a/src/test/java/rx/internal/util/IndexedRingBufferTest.java +++ b/src/test/java/rx/internal/util/IndexedRingBufferTest.java @@ -37,7 +37,6 @@ public class IndexedRingBufferTest { @Test public void add() { - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); list.add(new LSubscription(1)); list.add(new LSubscription(2)); @@ -49,7 +48,6 @@ public void add() { @Test public void removeEnd() { - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); list.add(new LSubscription(1)); int n2 = list.add(new LSubscription(2)); @@ -67,7 +65,6 @@ public void removeEnd() { @Test public void removeMiddle() { - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); list.add(new LSubscription(1)); int n2 = list.add(new LSubscription(2)); @@ -82,7 +79,6 @@ public void removeMiddle() { @Test public void addRemoveAdd() { - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); list.add("one"); list.add("two"); @@ -119,7 +115,6 @@ public void addRemoveAdd() { @Test public void addThousands() { String s = "s"; - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); for (int i = 0; i < 10000; i++) { list.add(s); @@ -145,7 +140,6 @@ public void addThousands() { @Test public void testForEachWithIndex() { - @SuppressWarnings("unchecked") IndexedRingBuffer buffer = IndexedRingBuffer.getInstance(); buffer.add("zero"); buffer.add("one"); @@ -212,7 +206,6 @@ public Boolean call(String t1) { @Test public void testForEachAcrossSections() { - @SuppressWarnings("unchecked") IndexedRingBuffer buffer = IndexedRingBuffer.getInstance(); for (int i = 0; i < 10000; i++) { buffer.add(i); @@ -231,7 +224,6 @@ public void testForEachAcrossSections() { @Test public void longRunningAddRemoveAddDoesntLeakMemory() { String s = "s"; - @SuppressWarnings("unchecked") IndexedRingBuffer list = IndexedRingBuffer.getInstance(); for (int i = 0; i < 20000; i++) { int index = list.add(s); @@ -242,14 +234,13 @@ public void longRunningAddRemoveAddDoesntLeakMemory() { list.forEach(newCounterAction(c)); assertEquals(0, c.get()); // System.out.println("Index is: " + list.index.get() + " when it should be no bigger than " + list.SIZE); - assertTrue(list.index.get() < list.SIZE); + assertTrue(list.index.get() < IndexedRingBuffer.SIZE); // it should actually be 1 since we only did add/remove sequentially assertEquals(1, list.index.get()); } @Test public void testConcurrentAdds() throws InterruptedException { - @SuppressWarnings("unchecked") final IndexedRingBuffer list = IndexedRingBuffer.getInstance(); Scheduler.Worker w1 = Schedulers.computation().createWorker(); @@ -300,7 +291,6 @@ public void call() { @Test public void testConcurrentAddAndRemoves() throws InterruptedException { - @SuppressWarnings("unchecked") final IndexedRingBuffer list = IndexedRingBuffer.getInstance(); final List exceptions = Collections.synchronizedList(new ArrayList()); From a6b70e0ac7220817c7d0f42c69dfa8cfde7cda84 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 9 Apr 2015 15:02:25 +1000 Subject: [PATCH 221/857] use simpler naming in Action1, Func1 because is used as a default for IDEs when generating implementing methods --- src/main/java/rx/functions/Action1.java | 4 ++-- src/main/java/rx/functions/Func1.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/functions/Action1.java b/src/main/java/rx/functions/Action1.java index e660036d5d..e3f906b6e6 100644 --- a/src/main/java/rx/functions/Action1.java +++ b/src/main/java/rx/functions/Action1.java @@ -18,6 +18,6 @@ /** * A one-argument action. */ -public interface Action1 extends Action { - void call(T1 t1); +public interface Action1 extends Action { + void call(T t); } diff --git a/src/main/java/rx/functions/Func1.java b/src/main/java/rx/functions/Func1.java index eb5d64fd24..54fbd98705 100644 --- a/src/main/java/rx/functions/Func1.java +++ b/src/main/java/rx/functions/Func1.java @@ -18,6 +18,6 @@ /** * Represents a function with one argument. */ -public interface Func1 extends Function { - R call(T1 t1); +public interface Func1 extends Function { + R call(T t); } From 6e5628d88827b2ad3b9f54b516642b05d7ba9a39 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 9 Apr 2015 17:11:23 +1000 Subject: [PATCH 222/857] remove unused code that was the subject of varargs warnings, remove unused import --- .../rx/internal/util/SubscriptionIndexedRingBuffer.java | 6 ------ src/main/java/rx/internal/util/SubscriptionRandomList.java | 7 +------ src/main/java/rx/plugins/RxJavaErrorHandler.java | 1 - 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java b/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java index 1c3e0abe3f..6dcb2d566d 100644 --- a/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java +++ b/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java @@ -35,12 +35,6 @@ public final class SubscriptionIndexedRingBuffer impleme public SubscriptionIndexedRingBuffer() { } - public SubscriptionIndexedRingBuffer(final T... subscriptions) { - for (T t : subscriptions) { - this.subscriptions.add(t); - } - } - @Override public boolean isUnsubscribed() { return unsubscribed == 1; diff --git a/src/main/java/rx/internal/util/SubscriptionRandomList.java b/src/main/java/rx/internal/util/SubscriptionRandomList.java index bc316f6abb..8883861cd4 100644 --- a/src/main/java/rx/internal/util/SubscriptionRandomList.java +++ b/src/main/java/rx/internal/util/SubscriptionRandomList.java @@ -16,14 +16,13 @@ package rx.internal.util; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import rx.Subscription; -import rx.exceptions.*; +import rx.exceptions.Exceptions; import rx.functions.Action1; /** @@ -39,10 +38,6 @@ public final class SubscriptionRandomList implements Sub public SubscriptionRandomList() { } - public SubscriptionRandomList(final T... subscriptions) { - this.subscriptions = new HashSet(Arrays.asList(subscriptions)); - } - @Override public synchronized boolean isUnsubscribed() { return unsubscribed; diff --git a/src/main/java/rx/plugins/RxJavaErrorHandler.java b/src/main/java/rx/plugins/RxJavaErrorHandler.java index 1003159bdb..85a21d447a 100644 --- a/src/main/java/rx/plugins/RxJavaErrorHandler.java +++ b/src/main/java/rx/plugins/RxJavaErrorHandler.java @@ -19,7 +19,6 @@ import rx.Subscriber; import rx.annotations.Experimental; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; /** * Abstract class for defining error handling logic in addition to the normal From 6fcdc7eb2a87eb6ed0c971e8076aadd3895662c8 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 9 Apr 2015 14:09:58 +0200 Subject: [PATCH 223/857] Fixed reentrancy issue with the error producer. --- .../internal/operators/OperatorRetryTest.java | 76 +++++++++++++++---- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index 4be71c834d..a5aa9f1c31 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -399,8 +399,9 @@ public void call(final Subscriber o) { public void request(long n) { if (n == Long.MAX_VALUE) { o.onNext("beginningEveryTime"); - if (count.getAndIncrement() < numFailures) { - o.onError(new RuntimeException("forced failure: " + count.get())); + int i = count.getAndIncrement(); + if (i < numFailures) { + o.onError(new RuntimeException("forced failure: " + (i + 1))); } else { o.onNext("onSuccessOnly"); o.onCompleted(); @@ -411,8 +412,7 @@ public void request(long n) { int i = count.getAndIncrement(); if (i < numFailures) { o.onNext("beginningEveryTime"); - o.onError(new RuntimeException("forced failure: " + count.get())); - req.decrementAndGet(); + o.onError(new RuntimeException("forced failure: " + (i + 1))); } else { do { if (i == numFailures) { @@ -705,17 +705,18 @@ public void testRetryWithBackpressure() throws InterruptedException { inOrder.verifyNoMoreInteractions(); } } + @Test(timeout = 15000) public void testRetryWithBackpressureParallel() throws InterruptedException { final int NUM_RETRIES = RxRingBuffer.SIZE * 2; int ncpu = Runtime.getRuntime().availableProcessors(); - ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 1)); + ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 2)); final AtomicInteger timeouts = new AtomicInteger(); final Map> data = new ConcurrentHashMap>(); final Map> exceptions = new ConcurrentHashMap>(); final Map completions = new ConcurrentHashMap(); - int m = 2000; + int m = 5000; final CountDownLatch cdl = new CountDownLatch(m); for (int i = 0; i < m; i++) { final int j = i; @@ -726,16 +727,17 @@ public void run() { try { Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); TestSubscriber ts = new TestSubscriber(); - origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); - ts.awaitTerminalEvent(2, TimeUnit.SECONDS); - if (ts.getOnNextEvents().size() != NUM_RETRIES + 2) { - data.put(j, ts.getOnNextEvents()); + origin.retry() + .observeOn(Schedulers.computation()).unsafeSubscribe(ts); + ts.awaitTerminalEvent(2500, TimeUnit.MILLISECONDS); + if (ts.getOnCompletedEvents().size() != 1) { + completions.put(j, ts.getOnCompletedEvents().size()); } if (ts.getOnErrorEvents().size() != 0) { exceptions.put(j, ts.getOnErrorEvents()); } - if (ts.getOnCompletedEvents().size() != 1) { - completions.put(j, ts.getOnCompletedEvents().size()); + if (ts.getOnNextEvents().size() != NUM_RETRIES + 2) { + data.put(j, ts.getOnNextEvents()); } } catch (Throwable t) { timeouts.incrementAndGet(); @@ -749,7 +751,16 @@ public void run() { cdl.await(); assertEquals(0, timeouts.get()); if (data.size() > 0) { - fail("Data content mismatch: " + data); + System.out.println(allSequenceFrequency(data)); + } + if (exceptions.size() > 0) { + System.out.println(exceptions); + } + if (completions.size() > 0) { + System.out.println(completions); + } + if (data.size() > 0) { + fail("Data content mismatch: " + allSequenceFrequency(data)); } if (exceptions.size() > 0) { fail("Exceptions received: " + exceptions); @@ -758,6 +769,45 @@ public void run() { fail("Multiple completions received: " + completions); } } + static StringBuilder allSequenceFrequency(Map> its) { + StringBuilder b = new StringBuilder(); + for (Map.Entry> e : its.entrySet()) { + if (b.length() > 0) { + b.append(", "); + } + b.append(e.getKey()).append("={"); + b.append(sequenceFrequency(e.getValue())); + b.append("}"); + } + return b; + } + static StringBuilder sequenceFrequency(Iterable it) { + StringBuilder sb = new StringBuilder(); + + Object prev = null; + int cnt = 0; + + for (Object curr : it) { + if (sb.length() > 0) { + if (!curr.equals(prev)) { + if (cnt > 1) { + sb.append(" x ").append(cnt); + cnt = 1; + } + sb.append(", "); + sb.append(curr); + } else { + cnt++; + } + } else { + sb.append(curr); + cnt++; + } + prev = curr; + } + + return sb; + } @Test(timeout = 3000) public void testIssue1900() throws InterruptedException { @SuppressWarnings("unchecked") From 135477ecb00bc2cc671ceaee42675441291ecfdd Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 9 Apr 2015 06:47:07 -0700 Subject: [PATCH 224/857] 1.0.9 --- CHANGES.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 738534fbe2..1a6920f16c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,26 @@ # RxJava Releases # +### Version 1.0.9 – April 9th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.9%7C)) ### + +* [Pull 2845] (https://github.com/ReactiveX/RxJava/pull/2845) Fix for repeat: wrong target of request +* [Pull 2851] (https://github.com/ReactiveX/RxJava/pull/2851) Add 'request(Long.MAX_VALUE)' in 'onStart' to fix the backpressure issue of debounce +* [Pull 2852] (https://github.com/ReactiveX/RxJava/pull/2852) Change retryWhen to eagerly ignore an error'd source's subsequent events +* [Pull 2859] (https://github.com/ReactiveX/RxJava/pull/2859) OperatorDoOnRequest should unsubscribe from upstream +* [Pull 2854] (https://github.com/ReactiveX/RxJava/pull/2854) Fixes wrong request accounting in AbstractOnSubscribe +* [Pull 2860] (https://github.com/ReactiveX/RxJava/pull/2860) OperatorSingle should request exactly what it needs +* [Pull 2817] (https://github.com/ReactiveX/RxJava/pull/2817) Fix for non-deterministic: testOnBackpressureDropWithAction +* [Pull 2818] (https://github.com/ReactiveX/RxJava/pull/2818) Small fix for the getValue javadoc in AsyncSubject and BehaviorSubject +* [Pull 2823] (https://github.com/ReactiveX/RxJava/pull/2823) Enable maven central sync via bintray +* [Pull 2807] (https://github.com/ReactiveX/RxJava/pull/2807) Corrected all Java interfaces declarations +* [Pull 2632] (https://github.com/ReactiveX/RxJava/pull/2632) Implement hook to render specific types in OnNextValue +* [Pull 2837] (https://github.com/ReactiveX/RxJava/pull/2837) Fixed a non-deterministic test and a few scheduler leaks (in tests) +* [Pull 2825] (https://github.com/ReactiveX/RxJava/pull/2825) Fixed javadoc for Observable.repeat() method +* [Pull 2838] (https://github.com/ReactiveX/RxJava/pull/2838) Fix typo in OnSubscribe interface's Javadoc +* [Pull 2864] (https://github.com/ReactiveX/RxJava/pull/2864) IndexedRingBuffer.getInstance can infer type +* [Pull 2862] (https://github.com/ReactiveX/RxJava/pull/2862) Cleanup warnings in test source +* [Pull 2868] (https://github.com/ReactiveX/RxJava/pull/2868) Fixed reentrancy issue with the error producer. +* [Pull 2866] (https://github.com/ReactiveX/RxJava/pull/2866) Use simpler naming in Action1, Func1 to assist IDEs + ### Version 1.0.8 – March 7th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.8%7C)) ### * [Pull 2809] (https://github.com/ReactiveX/RxJava/pull/2809) Fixed takeUntil not unsubscribing from either of the observables in case of a terminal condition. From 3355bddf6d02402afd43750aec0cdfb6766944bb Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 10 Apr 2015 14:16:14 +1000 Subject: [PATCH 225/857] use explicit versioning for gradle-rxjava-project-plugin --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 412ccb14d2..8b934db6eb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { repositories { jcenter() } - dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:2.+' } + dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:2.2.3' } } description = 'RxJava: Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.' From a5397735ba7dfa11035062f5d8d4750b8920b7bf Mon Sep 17 00:00:00 2001 From: Rob Spieldenner Date: Fri, 10 Apr 2015 09:51:10 -0700 Subject: [PATCH 226/857] Use the correct accounts for sonatype sync --- .travis.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8099e7d4bf..5c90183bda 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: java - jdk: - - oraclejdk7 - +- oraclejdk7 sudo: false # as per http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ @@ -14,11 +12,9 @@ cache: directories: - $HOME/.m2 - $HOME/.gradle - -# secure environment variables for release to Bintray env: global: - - secure: "HzRt91B6CLkBCxRXlo7V+F5L8SSHSW8dgkO8nAlTnOIK73k3QelDeBlcm1wnuNa3x+54YS9WBv6QrTNZr9lVi/8IPfKpd+jtjIbCWRvh6aNhqLIXWTL7oTvUd4E8DDUAKB6UMae6SiBSy2wsFELGHXeNwg7EiPfxsd5sKRiS7H4=" - - secure: "MSZLPasqNKAC+8qhKQD3xO+ZbuOy65HpUN+1+KnoOLMkHCu/f4x60W1tpTAzn1DFEVpokHR0n3I4z4HpWybURDQfDHD1bB4IsznjCUBYA9Uo9Sb0U4TS17dQr8s7SORIjHDLGNSWETJjrA9TfuUV6HTVhRO1ECx3H+wuTwCVDN0=" - - secure: Joj/k9B4q1BttgP7rY1DFR9flURcvT2b4PFnxYwxljQuu6NHwz/3yLM1b711Kv9oAXlo1D/ZTXsCzle8tLs5yC3GakDCpapqZP4Gmen4zGLuHB851gejH134dJj4bEWigrSM6NJMzjbl7qmlMAc8R+DlLi/J7AxNicOrhOT5MGw= - - secure: jxpzSkzSBnTqlAAY6r8QmX4b/Gf36NTshQ7xWQ8UWkWHHjm4GTnCoR71nXCIqhtZRgXvteR2AKYbraXU3ROGkZZXR4KkEwjhkf2FVr16bmUWbiqQrVvIdBPljcV9m3OevNEzCqd3QPod/Jma5s8WIDvuOv2z/cnpN/HQiHaRFEM= + - secure: YcLpYfNc/dyDON+oDvnJK5pFNhpPeJHxlAHV8JBt42e51prAl6njqrg1Qlfdp0pvBiskTPQHUxbFy9DOB1Z+43lPj5vlqz6qBgtS3vtBnsrczr+5Xx7NTdVKq6oZGl45VjfNPT7zdM6GQ5ifdzOid6kJIFu34g9JZkCzOY3BWGM= + - secure: WVmfSeW1UMNdem7+X4cVDjkEkqdeNavYH4udn3bFN1IFaWdliWFp4FYVBVi+p1T/IgkRSqzoW9Bm43DABe1UMFoErFCbfd7B0Ofgb4NZAsxFgokHGVLCe6k5+rQyASseiO7k0itSj3Kq9TrDueKPhv+g+IG0w1A8yZTnXdhXHvY= + - secure: Xt8E09nmSr+5r7ly95hG/EiBitZbhFGPRGp8oqPkNn1A2fzG9+hnvlNLgQhVPsISZGzJwkWa3LGBxAVGmuysVOz7eCwkoqlDZaaSLYAPfWXqkr+cmYGPkErgHSp+n/hnQG4TylX0YxzqX8flr6db21zWyNduiyHmo+xFydI5LeM= + - secure: RmpIsmYa5BdLLWR6DILjhEE/dx2q3O0NIkvnMx5G1cyRCNCrOf1B7fYFHnsTDwpvRA+6H6dZinmeyf6D3G+czOG5q/TW2jcu5nh+YOLhBb6jPIqRDfq/WHAa5Lkdssxs5g9RdWlEDVFMoE62lGc4cnfJz5F5puH29dy2SvXxIQw= From f8373d3b9bd94571bdfd9d5cac53d3ccd8a036a8 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 14 Apr 2015 11:25:43 +0200 Subject: [PATCH 227/857] Fixes NPE in requestFromChild method. --- src/main/java/rx/internal/operators/OperatorConcat.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index dc06b7561e..28fbfe5edc 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -114,8 +114,9 @@ public void onStart() { private void requestFromChild(long n) { // we track 'requested' so we know whether we should subscribe the next or not + ConcatInnerSubscriber actualSubscriber = currentSubscriber; if (REQUESTED_UPDATER.getAndAdd(this, n) == 0) { - if (currentSubscriber == null && wip > 0) { + if (actualSubscriber == null && wip > 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(); @@ -124,9 +125,9 @@ private void requestFromChild(long n) { } } - if (currentSubscriber != null) { + if (actualSubscriber != null) { // otherwise we are just passing it through to the currentSubscriber - currentSubscriber.requestMore(n); + actualSubscriber.requestMore(n); } } From d1e45ae455ea7b094761244ed00b4443b580d86b Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 15 Apr 2015 17:06:36 +1000 Subject: [PATCH 228/857] use singleton reduction functions in count and countLong --- src/main/java/rx/Observable.java | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c021331ff1..2d6e48eda2 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3630,12 +3630,16 @@ public final Boolean call(T t1) { * @see #countLong() */ public final Observable count() { - return reduce(0, new Func2() { + return reduce(0, CountHolder.INSTANCE); + } + + private static final class CountHolder { + static final Func2 INSTANCE = new Func2() { @Override - public final Integer call(Integer t1, T t2) { - return t1 + 1; + public final Integer call(Integer count, Object o) { + return count + 1; } - }); + }; } /** @@ -3657,14 +3661,18 @@ public final Integer call(Integer t1, T t2) { * @see #count() */ public final Observable countLong() { - return reduce(0L, new Func2() { + return reduce(0L, CountLongHolder.INSTANCE); + } + + private static final class CountLongHolder { + static final Func2 INSTANCE = new Func2() { @Override - public final Long call(Long t1, T t2) { - return t1 + 1; + 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. From 900beb1bf33641d44c0ff5e1b9acd52f2d7ba11b Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 16 Apr 2015 13:13:26 +1000 Subject: [PATCH 229/857] child.onNext should not be called after child.onError --- src/main/java/rx/internal/operators/OperatorScan.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index 92bb2175d2..788842100d 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -20,6 +20,7 @@ import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.exceptions.OnErrorThrowable; import rx.functions.Func0; import rx.functions.Func2; @@ -103,7 +104,9 @@ public void onNext(T currentValue) { try { this.value = accumulator.call(this.value, currentValue); } catch (Throwable e) { + Exceptions.throwIfFatal(e); child.onError(OnErrorThrowable.addValueAsLastCause(e, currentValue)); + return; } } child.onNext(this.value); From 5e47f78712b7a3b0f018554a4661fc6535c907c7 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 16 Apr 2015 20:42:46 +0200 Subject: [PATCH 230/857] Proposal: standardized Subject state-peeking methods. --- src/main/java/rx/subjects/AsyncSubject.java | 27 ++ .../java/rx/subjects/BehaviorSubject.java | 25 ++ src/main/java/rx/subjects/PublishSubject.java | 27 ++ src/main/java/rx/subjects/ReplaySubject.java | 66 ++- .../java/rx/subjects/SerializedSubject.java | 36 ++ src/main/java/rx/subjects/Subject.java | 89 ++++ .../rx/subjects/SerializedSubjectTest.java | 384 ++++++++++++++++++ 7 files changed, 645 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index 5d668f1be6..c186b1f78c 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -15,6 +15,7 @@ */ package rx.subjects; +import java.lang.reflect.Array; import java.util.*; import rx.Observer; @@ -141,6 +142,7 @@ public boolean hasObservers() { * @return true if and only if the subject has some value but not an error */ @Experimental + @Override public boolean hasValue() { Object v = lastValue; Object o = state.get(); @@ -151,6 +153,7 @@ public boolean hasValue() { * @return true if the subject has received a throwable through {@code onError}. */ @Experimental + @Override public boolean hasThrowable() { Object o = state.get(); return nl.isError(o); @@ -160,6 +163,7 @@ public boolean hasThrowable() { * @return true if the subject completed normally via {@code onCompleted()} */ @Experimental + @Override public boolean hasCompleted() { Object o = state.get(); return o != null && !nl.isError(o); @@ -174,6 +178,7 @@ public boolean hasCompleted() { * has terminated with an exception or has an actual {@code null} as a value. */ @Experimental + @Override public T getValue() { Object v = lastValue; Object o = state.get(); @@ -188,6 +193,7 @@ public T getValue() { * subject hasn't terminated yet or it terminated normally. */ @Experimental + @Override public Throwable getThrowable() { Object o = state.get(); if (nl.isError(o)) { @@ -195,4 +201,25 @@ public Throwable getThrowable() { } return null; } + @Override + @Experimental + @SuppressWarnings("unchecked") + public T[] getValues(T[] a) { + Object v = lastValue; + Object o = state.get(); + if (!nl.isError(o) && nl.isNext(v)) { + T val = nl.getValue(v); + if (a.length == 0) { + a = (T[])Array.newInstance(a.getClass().getComponentType(), 1); + } + a[0] = val; + if (a.length > 1) { + a[1] = null; + } + } else + if (a.length > 0) { + a[0] = null; + } + return a; + } } diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index f2a47a8d17..218eef5eba 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -16,6 +16,7 @@ package rx.subjects; +import java.lang.reflect.Array; import java.util.*; import rx.Observer; @@ -177,6 +178,7 @@ public boolean hasObservers() { * @return true if and only if the subject has some value and hasn't terminated yet. */ @Experimental + @Override public boolean hasValue() { Object o = state.get(); return nl.isNext(o); @@ -186,6 +188,7 @@ public boolean hasValue() { * @return true if the subject has received a throwable through {@code onError}. */ @Experimental + @Override public boolean hasThrowable() { Object o = state.get(); return nl.isError(o); @@ -195,6 +198,7 @@ public boolean hasThrowable() { * @return true if the subject completed normally via {@code onCompleted()} */ @Experimental + @Override public boolean hasCompleted() { Object o = state.get(); return nl.isCompleted(o); @@ -209,6 +213,7 @@ public boolean hasCompleted() { * has terminated or has an actual {@code null} as a valid value. */ @Experimental + @Override public T getValue() { Object o = state.get(); if (nl.isNext(o)) { @@ -222,6 +227,7 @@ public T getValue() { * subject hasn't terminated yet or it terminated normally. */ @Experimental + @Override public Throwable getThrowable() { Object o = state.get(); if (nl.isError(o)) { @@ -229,4 +235,23 @@ public Throwable getThrowable() { } return null; } + @Override + @Experimental + @SuppressWarnings("unchecked") + public T[] getValues(T[] a) { + Object o = state.get(); + if (nl.isNext(o)) { + if (a.length == 0) { + a = (T[])Array.newInstance(a.getClass().getComponentType(), 1); + } + a[0] = nl.getValue(o); + if (a.length > 1) { + a[1] = null; + } + } else + if (a.length > 0) { + a[0] = null; + } + return a; + } } diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index 0a3292ae50..1197048c3f 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -125,6 +125,7 @@ public boolean hasObservers() { * @return true if the subject has received a throwable through {@code onError}. */ @Experimental + @Override public boolean hasThrowable() { Object o = state.get(); return nl.isError(o); @@ -134,6 +135,7 @@ public boolean hasThrowable() { * @return true if the subject completed normally via {@code onCompleted} */ @Experimental + @Override public boolean hasCompleted() { Object o = state.get(); return o != null && !nl.isError(o); @@ -144,6 +146,7 @@ public boolean hasCompleted() { * subject hasn't terminated yet or it terminated normally. */ @Experimental + @Override public Throwable getThrowable() { Object o = state.get(); if (nl.isError(o)) { @@ -151,4 +154,28 @@ public Throwable getThrowable() { } return null; } + + @Override + @Experimental + public boolean hasValue() { + return false; + } + @Override + @Experimental + public T getValue() { + return null; + } + @Override + @Experimental + public Object[] getValues() { + return new Object[0]; + } + @Override + @Experimental + public T[] getValues(T[] a) { + if (a.length > 0) { + a[0] = null; + } + return a; + } } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index 5d6292be19..418a7d7f4e 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -550,12 +550,30 @@ public T[] toArray(T[] a) { for (int i = 0; i < s; i++) { a[i] = (T)list.get(i); } - if (s < a.length - 1) { + if (a.length > s) { a[s] = null; } + } else + if (a.length > 0) { + a[0] = null; } return a; } + @Override + public T latest() { + int idx = index; + if (idx > 0) { + Object o = list.get(idx - 1); + if (nl.isCompleted(o) || nl.isError(o)) { + if (idx > 1) { + return nl.getValue(list.get(idx - 2)); + } + return null; + } + return nl.getValue(o); + } + return null; + } } @@ -715,6 +733,27 @@ public T[] toArray(T[] a) { } return list.toArray(a); } + @Override + public T latest() { + Node h = head().next; + if (h == null) { + return null; + } + Node p = null; + while (h != tail()) { + p = h; + h = h.next; + } + Object value = leaveTransform.call(h.value); + if (nl.isError(value) || nl.isCompleted(value)) { + if (p != null) { + value = leaveTransform.call(p.value); + return nl.getValue(value); + } + return null; + } + return nl.getValue(value); + } } // ************** @@ -781,6 +820,12 @@ I replayObserverFromIndexTest( * @return the array or a new array containing the current values */ T[] toArray(T[] a); + /** + * Returns the latest value that has been buffered or null if no such value + * present. + * @return the latest value buffered or null if none + */ + T latest(); } /** Interface to manage eviction checking. */ @@ -1054,6 +1099,7 @@ public void evictFinal(NodeList list) { * @return true if the subject has received a throwable through {@code onError}. */ @Experimental + @Override public boolean hasThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.get(); @@ -1064,6 +1110,7 @@ public boolean hasThrowable() { * @return true if the subject completed normally via {@code onCompleted} */ @Experimental + @Override public boolean hasCompleted() { NotificationLite nl = ssm.nl; Object o = ssm.get(); @@ -1075,6 +1122,7 @@ public boolean hasCompleted() { * subject hasn't terminated yet or it terminated normally. */ @Experimental + @Override public Throwable getThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.get(); @@ -1098,15 +1146,10 @@ public int size() { public boolean hasAnyValue() { return !state.isEmpty(); } - /** An empty array to trigger getValues() to return a new array. */ - private static final Object[] EMPTY_ARRAY = new Object[0]; - /** - * @return returns a snapshot of the currently buffered non-terminal events. - */ - @SuppressWarnings("unchecked") @Experimental - public Object[] getValues() { - return state.toArray((T[])EMPTY_ARRAY); + @Override + public boolean hasValue() { + return hasAnyValue(); } /** * Returns a snapshot of the currently buffered non-terminal events into @@ -1115,7 +1158,12 @@ public Object[] getValues() { * @return the array {@code a} if it had enough capacity or a new array containing the available values */ @Experimental + @Override public T[] getValues(T[] a) { return state.toArray(a); } + @Override + public T getValue() { + return state.latest(); + } } diff --git a/src/main/java/rx/subjects/SerializedSubject.java b/src/main/java/rx/subjects/SerializedSubject.java index 5a86a35137..baaf50b8d4 100644 --- a/src/main/java/rx/subjects/SerializedSubject.java +++ b/src/main/java/rx/subjects/SerializedSubject.java @@ -16,6 +16,7 @@ package rx.subjects; import rx.Subscriber; +import rx.annotations.Experimental; import rx.observers.SerializedObserver; /** @@ -68,4 +69,39 @@ public void onNext(T t) { public boolean hasObservers() { return actual.hasObservers(); } + @Override + @Experimental + public boolean hasCompleted() { + return actual.hasCompleted(); + } + @Override + @Experimental + public boolean hasThrowable() { + return actual.hasThrowable(); + } + @Override + @Experimental + public boolean hasValue() { + return actual.hasValue(); + } + @Override + @Experimental + public Throwable getThrowable() { + return actual.getThrowable(); + } + @Override + @Experimental + public T getValue() { + return actual.getValue(); + } + @Override + @Experimental + public Object[] getValues() { + return actual.getValues(); + } + @Override + @Experimental + public T[] getValues(T[] a) { + return actual.getValues(a); + } } diff --git a/src/main/java/rx/subjects/Subject.java b/src/main/java/rx/subjects/Subject.java index 525893a82f..5aa8ba4b85 100644 --- a/src/main/java/rx/subjects/Subject.java +++ b/src/main/java/rx/subjects/Subject.java @@ -18,6 +18,7 @@ import rx.Observable; import rx.Observer; import rx.Subscriber; +import rx.annotations.Experimental; /** * Represents an object that is both an Observable and an Observer. @@ -49,6 +50,94 @@ protected Subject(OnSubscribe onSubscribe) { * @return SerializedSubject wrapping the current Subject */ public final SerializedSubject toSerialized() { + if (getClass() == SerializedSubject.class) { + return (SerializedSubject)this; + } return new SerializedSubject(this); } + /** + * Check if the Subject has terminated with an exception. + *

The operation is threadsafe. + * @return true if the subject has received a throwable through {@code onError}. + */ + @Experimental + public boolean hasThrowable() { + throw new UnsupportedOperationException(); + } + /** + * Check if the Subject has terminated normally. + *

The operation is threadsafe. + * @return true if the subject completed normally via {@code onCompleted} + */ + @Experimental + public boolean hasCompleted() { + throw new UnsupportedOperationException(); + } + /** + * Returns the Throwable that terminated the Subject. + *

The operation is threadsafe. + * @return the Throwable that terminated the Subject or {@code null} if the + * subject hasn't terminated yet or it terminated normally. + */ + @Experimental + public Throwable getThrowable() { + throw new UnsupportedOperationException(); + } + /** + * Check if the Subject has any value. + *

Use the {@link #getValue()} method to retrieve such a value. + *

Note that unless {@link #hasCompleted()} or {@link #hasThrowable()} returns true, the value + * retrieved by {@code getValue()} may get outdated. + *

The operation is threadsafe. + * @return true if and only if the subject has some value but not an error + */ + @Experimental + public boolean hasValue() { + throw new UnsupportedOperationException(); + } + /** + * Returns the current or latest value of the Subject if there is such a value and + * the subject hasn't terminated with an exception. + *

The method can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} + * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an + * exception or the Subject terminated without receiving any value. + *

The operation is threadsafe. + * @return the current value or {@code null} if the Subject doesn't have a value, + * has terminated with an exception or has an actual {@code null} as a value. + */ + @Experimental + public T getValue() { + throw new UnsupportedOperationException(); + } + /** An empty array to trigger getValues() to return a new array. */ + private static final Object[] EMPTY_ARRAY = new Object[0]; + /** + * Returns a snapshot of the currently buffered non-terminal events. + *

The operation is threadsafe. + * @return a snapshot of the currently buffered non-terminal events. + */ + @SuppressWarnings("unchecked") + @Experimental + public Object[] getValues() { + T[] r = getValues((T[])EMPTY_ARRAY); + if (r == EMPTY_ARRAY) { + return new Object[0]; // don't leak the default empty array. + } + return r; + } + /** + * Returns a snapshot of the currently buffered non-terminal events into + * the provided {@code a} array or creates a new array if it has not enough capacity. + *

If the subject's values fit in the specified array with room to spare + * (i.e., the array has more elements than the list), the element in + * the array immediately following the end of the subject's values is set to + * null. + *

The operation is threadsafe. + * @param a the array to fill in + * @return the array {@code a} if it had enough capacity or a new array containing the available values + */ + @Experimental + public T[] getValues(T[] a) { + throw new UnsupportedOperationException(); + } } diff --git a/src/test/java/rx/subjects/SerializedSubjectTest.java b/src/test/java/rx/subjects/SerializedSubjectTest.java index bba2ce1e5d..097fcd311e 100644 --- a/src/test/java/rx/subjects/SerializedSubjectTest.java +++ b/src/test/java/rx/subjects/SerializedSubjectTest.java @@ -15,10 +15,13 @@ */ package rx.subjects; +import static org.junit.Assert.*; + import java.util.Arrays; import org.junit.Test; +import rx.exceptions.TestException; import rx.observers.TestSubscriber; public class SerializedSubjectTest { @@ -33,4 +36,385 @@ public void testBasic() { ts.awaitTerminalEvent(); ts.assertReceivedOnNext(Arrays.asList("hello")); } + + @Test + public void testAsyncSubjectValueRelay() { + AsyncSubject async = AsyncSubject.create(); + async.onNext(1); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, serial.getValue()); + assertTrue(serial.hasValue()); + assertArrayEquals(new Object[] { 1 }, serial.getValues()); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testAsyncSubjectValueEmpty() { + AsyncSubject async = AsyncSubject.create(); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testAsyncSubjectValueError() { + AsyncSubject async = AsyncSubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testPublishSubjectValueRelay() { + PublishSubject async = PublishSubject.create(); + async.onNext(1); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + + assertArrayEquals(new Object[0], serial.getValues()); + assertArrayEquals(new Integer[0], serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testPublishSubjectValueEmpty() { + PublishSubject async = PublishSubject.create(); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testPublishSubjectValueError() { + PublishSubject async = PublishSubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testBehaviorSubjectValueRelay() { + BehaviorSubject async = BehaviorSubject.create(); + async.onNext(1); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectValueRelayIncomplete() { + BehaviorSubject async = BehaviorSubject.create(); + async.onNext(1); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, serial.getValue()); + assertTrue(serial.hasValue()); + assertArrayEquals(new Object[] { 1 }, serial.getValues()); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectIncompleteEmpty() { + BehaviorSubject async = BehaviorSubject.create(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectEmpty() { + BehaviorSubject async = BehaviorSubject.create(); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectError() { + BehaviorSubject async = BehaviorSubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testReplaySubjectValueRelay() { + ReplaySubject async = ReplaySubject.create(); + async.onNext(1); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, serial.getValue()); + assertTrue(serial.hasValue()); + assertArrayEquals(new Object[] { 1 }, serial.getValues()); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayIncomplete() { + ReplaySubject async = ReplaySubject.create(); + async.onNext(1); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, serial.getValue()); + assertTrue(serial.hasValue()); + assertArrayEquals(new Object[] { 1 }, serial.getValues()); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayBounded() { + ReplaySubject async = ReplaySubject.createWithSize(1); + async.onNext(0); + async.onNext(1); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, serial.getValue()); + assertTrue(serial.hasValue()); + assertArrayEquals(new Object[] { 1 }, serial.getValues()); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayBoundedIncomplete() { + ReplaySubject async = ReplaySubject.createWithSize(1); + async.onNext(0); + async.onNext(1); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, serial.getValue()); + assertTrue(serial.hasValue()); + assertArrayEquals(new Object[] { 1 }, serial.getValues()); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayBoundedEmptyIncomplete() { + ReplaySubject async = ReplaySubject.createWithSize(1); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayEmptyIncomplete() { + ReplaySubject async = ReplaySubject.create(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testReplaySubjectEmpty() { + ReplaySubject async = ReplaySubject.create(); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectError() { + ReplaySubject async = ReplaySubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testReplaySubjectBoundedEmpty() { + ReplaySubject async = ReplaySubject.createWithSize(1); + async.onCompleted(); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasCompleted()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectBoundedError() { + ReplaySubject async = ReplaySubject.createWithSize(1); + TestException te = new TestException(); + async.onError(te); + Subject serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasCompleted()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(serial.getValue()); + assertFalse(serial.hasValue()); + assertArrayEquals(new Object[] { }, serial.getValues()); + assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testDontWrapSerializedSubjectAgain() { + PublishSubject s = PublishSubject.create(); + Subject s1 = s.toSerialized(); + Subject s2 = s1.toSerialized(); + assertSame(s1, s2); + } } From f8f4cd0944689bfc4d8cc39f6066e9c3c4660105 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sun, 19 Apr 2015 22:01:43 +1000 Subject: [PATCH 231/857] fix race condition for range where two threads concurrently request Long.MAX_VALUE and both start the fast path thus possibly some items more than once --- src/main/java/rx/internal/operators/OnSubscribeRange.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRange.java b/src/main/java/rx/internal/operators/OnSubscribeRange.java index f648e6f414..bcfbe0736b 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRange.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRange.java @@ -55,12 +55,11 @@ private RangeProducer(Subscriber o, int start, int end) { @Override public void request(long n) { - if (REQUESTED_UPDATER.get(this) == Long.MAX_VALUE) { + if (requested == Long.MAX_VALUE) { // already started with fast-path return; } - if (n == Long.MAX_VALUE) { - REQUESTED_UPDATER.set(this, n); + if (n == Long.MAX_VALUE && REQUESTED_UPDATER.compareAndSet(this, 0, Long.MAX_VALUE)) { // fast-path without backpressure for (long i = index; i <= end; i++) { if (o.isUnsubscribed()) { From 8d5d179ba0685d9a2674b18c141b9ff0e655fd34 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 20 Apr 2015 09:41:57 +0200 Subject: [PATCH 232/857] Concat: fixed reentrancy problem in completeInner --- .../rx/internal/operators/OperatorConcat.java | 2 +- .../operators/OperatorConcatTest.java | 66 ++++++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index 28fbfe5edc..f1a429dea4 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -158,11 +158,11 @@ public void onCompleted() { } void completeInner() { - request(1); currentSubscriber = null; if (WIP_UPDATER.decrementAndGet(this) > 0) { subscribeNext(); } + request(1); } void subscribeNext() { diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 688b0331f2..5ad80c6d70 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -28,22 +28,20 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; import org.junit.Test; import org.mockito.InOrder; -import rx.Observable; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Subscriber; -import rx.Subscription; +import rx.*; +import rx.functions.Func1; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.schedulers.TestScheduler; +import rx.subjects.Subject; import rx.subscriptions.BooleanSubscription; public class OperatorConcatTest { @@ -485,11 +483,11 @@ public boolean isUnsubscribed() { private final T seed; private final int size; - public TestObservable(@SuppressWarnings("unchecked") T... values) { + public TestObservable(T... values) { this(null, null, values); } - public TestObservable(CountDownLatch once, CountDownLatch okToContinue, @SuppressWarnings("unchecked") T... values) { + public TestObservable(CountDownLatch once, CountDownLatch okToContinue, T... values) { this.values = Arrays.asList(values); this.size = this.values.size(); this.once = once; @@ -718,4 +716,54 @@ public void call(Subscriber s) { ts.assertReceivedOnNext(Arrays.asList("hello", "hello")); } + @Test(timeout = 10000) + public void testIssue2890NoStackoverflow() throws InterruptedException { + final ExecutorService executor = Executors.newFixedThreadPool(2); + final Scheduler sch = Schedulers.from(executor); + + Func1> func = new Func1>() { + @Override + public Observable call(Integer t) { + Observable observable = Observable.just(t) + .subscribeOn(sch) + ; + Subject subject = BufferUntilSubscriber.create(); + observable.subscribe(subject); + return subject; + } + }; + + int n = 5000; + final AtomicInteger counter = new AtomicInteger(); + + Observable.range(1, n).concatMap(func).subscribe(new Subscriber() { + @Override + public void onNext(Integer t) { + // Consume after sleep for 1 ms + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignored + } + if (counter.getAndIncrement() % 100 == 0) { + System.out.print("testIssue2890NoStackoverflow -> "); + System.out.println(counter.get()); + }; + } + + @Override + public void onCompleted() { + executor.shutdown(); + } + + @Override + public void onError(Throwable e) { + executor.shutdown(); + } + }); + + executor.awaitTermination(12000, TimeUnit.MILLISECONDS); + + assertEquals(n, counter.get()); + } } From adaa9131616ed60e85973f4e952adcffcd211ceb Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 20 Apr 2015 18:11:47 +1000 Subject: [PATCH 233/857] fix race condition for Observable.from(Iterable) where two concurrent calls to the Producer.request with Long.MAX_VALUE could start the fast path twice --- .../rx/internal/operators/OnSubscribeFromIterable.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index 4a27013b89..e4589a4f57 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -62,12 +62,11 @@ private IterableProducer(Subscriber o, Iterator it) { @Override public void request(long n) { - if (REQUESTED_UPDATER.get(this) == Long.MAX_VALUE) { + if (requested == Long.MAX_VALUE) { // already started with fast-path return; } - if (n == Long.MAX_VALUE) { - REQUESTED_UPDATER.set(this, n); + if (n == Long.MAX_VALUE && REQUESTED_UPDATER.compareAndSet(this, 0, Long.MAX_VALUE)) { // fast-path without backpressure while (it.hasNext()) { if (o.isUnsubscribed()) { @@ -78,7 +77,7 @@ public void request(long n) { if (!o.isUnsubscribed()) { o.onCompleted(); } - } else if(n > 0) { + } else if (n > 0) { // backpressure is requested long _c = BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER, this, n); if (_c == 0) { From 1f2ece0b87643506a9d88175b23b513ea60e148b Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 20 Apr 2015 21:23:27 +1000 Subject: [PATCH 234/857] add unit test to ensure that range with count of 0 sends onComplete even when initial request is 0 --- .../operators/OnSubscribeRangeTest.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java index 9b06cdb4d0..aea7a36730 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -26,6 +27,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; @@ -77,7 +79,7 @@ public void call(Integer t1) { } @Test - public void testRangeWithOverflow() { + public void testRangeWithZero() { Observable.range(1, 0); } @@ -220,4 +222,31 @@ public void onNext(Integer t) { }}); assertEquals(n, count.get()); } + + @Test + public void testEmptyRangeSendsOnCompleteEagerlyWithRequestZero() { + final AtomicBoolean completed = new AtomicBoolean(false); + Observable.range(1, 0).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(0); + } + + @Override + public void onCompleted() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + + }}); + assertTrue(completed.get()); + } } From 1c29e7c9b4d0f9ecfa3e69f294a0b2924db54cae Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 21 Apr 2015 10:00:32 +1000 Subject: [PATCH 235/857] Observable.from(iterable) should emit onCompleted even if none requested when iterable is empty --- .../operators/OnSubscribeFromIterable.java | 5 ++- .../OnSubscribeFromIterableTest.java | 31 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index e4589a4f57..766b624416 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -44,7 +44,10 @@ public OnSubscribeFromIterable(Iterable iterable) { @Override public void call(final Subscriber o) { final Iterator it = is.iterator(); - o.setProducer(new IterableProducer(o, it)); + if (!it.hasNext() && !o.isUnsubscribed()) + o.onCompleted(); + else + o.setProducer(new IterableProducer(o, it)); } private static final class IterableProducer implements Producer { diff --git a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java index b9f829783a..91bf65bf4d 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java @@ -27,6 +27,7 @@ import java.util.Iterator; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import org.mockito.Mockito; @@ -74,12 +75,12 @@ public Iterator iterator() { @Override public boolean hasNext() { - return i++ < 3; + return i < 3; } @Override public String next() { - return String.valueOf(i); + return String.valueOf(++i); } @Override @@ -193,5 +194,31 @@ public void onNext(Integer t) { assertTrue(latch.await(10, TimeUnit.SECONDS)); } + @Test + public void testFromEmptyIterableWhenZeroRequestedShouldStillEmitOnCompletedEagerly() { + final AtomicBoolean completed = new AtomicBoolean(false); + Observable.from(Collections.emptyList()).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(0); + } + + @Override + public void onCompleted() { + completed.set(true); + } + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Object t) { + + }}); + assertTrue(completed.get()); + } + } From 615db6a15d8ef5dc8be01e963d058c18fe853a10 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 21 Apr 2015 13:06:21 +0200 Subject: [PATCH 236/857] Operators toList and toSortedList now support backpressure --- src/main/java/rx/Observable.java | 63 +++++++++++- .../operators/OperatorToObservableList.java | 55 ++++++----- .../OperatorToObservableSortedList.java | 79 ++++++++------- .../operators/SingleDelayedProducer.java | 87 ++++++++++++++++ .../OperatorToObservableListTest.java | 85 +++++++++++++++- .../OperatorToObservableSortedListTest.java | 98 ++++++++++++++++--- 6 files changed, 390 insertions(+), 77 deletions(-) create mode 100644 src/main/java/rx/internal/operators/SingleDelayedProducer.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c021331ff1..078c4fc9b0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -8578,7 +8578,7 @@ public final BlockingObservable toBlocking() { * you do not have the option to unsubscribe. *
*
Backpressure Support:
- *
This operator does not support backpressure as by intent it is requesting and buffering everything.
+ *
The operator buffers everything from its upstream but it only emits the aggregated list when the downstream requests at least one item.
*
Scheduler:
*
{@code toList} does not operate by default on a particular {@link Scheduler}.
*
@@ -8779,7 +8779,7 @@ public final Observable>> toMultimap(Func1 *
*
Backpressure Support:
- *
This operator does not support backpressure as by intent it is requesting and buffering everything.
+ *
The operator buffers everything from its upstream but it only emits the sorted list when the downstream requests at least one item.
*
Scheduler:
*
{@code toSortedList} does not operate by default on a particular {@link Scheduler}.
*
@@ -8792,7 +8792,7 @@ public final Observable>> toMultimap(Func1ReactiveX operators documentation: To */ public final Observable> toSortedList() { - return lift(new OperatorToObservableSortedList()); + return lift(new OperatorToObservableSortedList(10)); } /** @@ -8802,7 +8802,7 @@ public final Observable> toSortedList() { * *
*
Backpressure Support:
- *
This operator does not support backpressure as by intent it is requesting and buffering everything.
+ *
The operator buffers everything from its upstream but it only emits the sorted list when the downstream requests at least one item.
*
Scheduler:
*
{@code toSortedList} does not operate by default on a particular {@link Scheduler}.
*
@@ -8815,7 +8815,60 @@ public final Observable> toSortedList() { * @see ReactiveX operators documentation: To */ public final Observable> toSortedList(Func2 sortFunction) { - return lift(new OperatorToObservableSortedList(sortFunction)); + return lift(new OperatorToObservableSortedList(sortFunction, 10)); + } + + /** + * Returns an Observable that emits a list that contains the items emitted by the source Observable, in a + * sorted order. Each item emitted by the Observable must implement {@link Comparable} with respect to all + * other items in the sequence. + *

+ * + *

+ *
Backpressure Support:
+ *
The operator buffers everything from its upstream but it only emits the sorted list when the downstream requests at least one item.
+ *
Scheduler:
+ *
{@code toSortedList} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @throws ClassCastException + * if any item emitted by the Observable does not implement {@link Comparable} with respect to + * all other items emitted by the Observable + * @param initialCapacity + * the initial capacity of the ArrayList used to accumulate items before sorting + * @return an Observable that emits a list that contains the items emitted by the source Observable in + * sorted order + * @see ReactiveX operators documentation: To + */ + @Experimental + public final Observable> toSortedList(int initialCapacity) { + return lift(new OperatorToObservableSortedList(initialCapacity)); + } + + /** + * Returns an Observable that emits a list that contains the items emitted by the source Observable, in a + * sorted order based on a specified comparison function. + *

+ * + *

+ *
Backpressure Support:
+ *
The operator buffers everything from its upstream but it only emits the sorted list when the downstream requests at least one item.
+ *
Scheduler:
+ *
{@code toSortedList} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param sortFunction + * a function that compares two items emitted by the source Observable and returns an Integer + * that indicates their sort order + * @param initialCapacity + * the initial capacity of the ArrayList used to accumulate items before sorting + * @return an Observable that emits a list that contains the items emitted by the source Observable in + * sorted order + * @see ReactiveX operators documentation: To + */ + @Experimental + public final Observable> toSortedList(Func2 sortFunction, int initialCapacity) { + return lift(new OperatorToObservableSortedList(sortFunction, initialCapacity)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorToObservableList.java b/src/main/java/rx/internal/operators/OperatorToObservableList.java index fef13577cc..8d7dff8f96 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableList.java @@ -52,10 +52,11 @@ public static OperatorToObservableList instance() { private OperatorToObservableList() { } @Override public Subscriber call(final Subscriber> o) { - return new Subscriber(o) { + final SingleDelayedProducer> producer = new SingleDelayedProducer>(o); + Subscriber result = new Subscriber() { - private boolean completed = false; - final List list = new LinkedList(); + boolean completed = false; + List list = new LinkedList(); @Override public void onStart() { @@ -64,27 +65,32 @@ public void onStart() { @Override public void onCompleted() { - try { + if (!completed) { completed = true; - /* - * Ideally this should just return Collections.unmodifiableList(list) and not copy it, - * but, it ends up being a breaking change if we make that modification. - * - * Here is an example of is being done with these lists that breaks if we make it immutable: - * - * Caused by: java.lang.UnsupportedOperationException - * at java.util.Collections$UnmodifiableList$1.set(Collections.java:1244) - * at java.util.Collections.sort(Collections.java:221) - * ... - * Caused by: rx.exceptions.OnErrorThrowable$OnNextValue: OnError while emitting onNext value: UnmodifiableList.class - * at rx.exceptions.OnErrorThrowable.addValueAsLastCause(OnErrorThrowable.java:98) - * at rx.internal.operators.OperatorMap$1.onNext(OperatorMap.java:56) - * ... 419 more - */ - o.onNext(new ArrayList(list)); - o.onCompleted(); - } catch (Throwable e) { - onError(e); + List result; + try { + /* + * Ideally this should just return Collections.unmodifiableList(list) and not copy it, + * but, it ends up being a breaking change if we make that modification. + * + * Here is an example of is being done with these lists that breaks if we make it immutable: + * + * Caused by: java.lang.UnsupportedOperationException + * at java.util.Collections$UnmodifiableList$1.set(Collections.java:1244) + * at java.util.Collections.sort(Collections.java:221) + * ... + * Caused by: rx.exceptions.OnErrorThrowable$OnNextValue: OnError while emitting onNext value: UnmodifiableList.class + * at rx.exceptions.OnErrorThrowable.addValueAsLastCause(OnErrorThrowable.java:98) + * at rx.internal.operators.OperatorMap$1.onNext(OperatorMap.java:56) + * ... 419 more + */ + result = new ArrayList(list); + } catch (Throwable t) { + onError(t); + return; + } + list = null; + producer.set(result); } } @@ -101,6 +107,9 @@ public void onNext(T value) { } }; + o.add(result); + o.setProducer(producer); + return result; } } diff --git a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java index afe9d3ee94..f2d5cb9948 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java @@ -15,13 +15,10 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.util.*; import rx.Observable.Operator; -import rx.Subscriber; +import rx.*; import rx.functions.Func2; /** @@ -35,23 +32,33 @@ * the type of the items emitted by the source and the resulting {@code Observable}s */ public final class OperatorToObservableSortedList implements Operator, T> { - private final Func2 sortFunction; + private final Comparator sortFunction; + private final int initialCapacity; @SuppressWarnings("unchecked") - public OperatorToObservableSortedList() { - this.sortFunction = defaultSortFunction; + public OperatorToObservableSortedList(int initialCapacity) { + this.sortFunction = DEFAULT_SORT_FUNCTION; + this.initialCapacity = initialCapacity; } - public OperatorToObservableSortedList(Func2 sortFunction) { - this.sortFunction = sortFunction; + public OperatorToObservableSortedList(final Func2 sortFunction, int initialCapacity) { + this.initialCapacity = initialCapacity; + this.sortFunction = new Comparator() { + @Override + public int compare(T o1, T o2) { + return sortFunction.call(o1, o2); + } + }; } @Override - public Subscriber call(final Subscriber> o) { - return new Subscriber(o) { - - final List list = new ArrayList(); + public Subscriber call(final Subscriber> child) { + final SingleDelayedProducer> producer = new SingleDelayedProducer>(child); + Subscriber result = new Subscriber() { + List list = new ArrayList(initialCapacity); + boolean completed; + @Override public void onStart() { request(Long.MAX_VALUE); @@ -59,48 +66,48 @@ public void onStart() { @Override public void onCompleted() { - try { - - // sort the list before delivery - Collections.sort(list, new Comparator() { - - @Override - public int compare(T o1, T o2) { - return sortFunction.call(o1, o2); - } - - }); - - o.onNext(Collections.unmodifiableList(list)); - o.onCompleted(); - } catch (Throwable e) { - onError(e); + if (!completed) { + completed = true; + List a = list; + list = null; + try { + // sort the list before delivery + Collections.sort(a, sortFunction); + } catch (Throwable e) { + onError(e); + return; + } + producer.set(a); } } @Override public void onError(Throwable e) { - o.onError(e); + child.onError(e); } @Override public void onNext(T value) { - list.add(value); + if (!completed) { + list.add(value); + } } }; + child.add(result); + child.setProducer(producer); + return result; } - // raw because we want to support Object for this default @SuppressWarnings("rawtypes") - private static Func2 defaultSortFunction = new DefaultComparableFunction(); + private static Comparator DEFAULT_SORT_FUNCTION = new DefaultComparableFunction(); - private static class DefaultComparableFunction implements Func2 { + private static class DefaultComparableFunction implements Comparator { // unchecked because we want to support Object for this default @SuppressWarnings("unchecked") @Override - public Integer call(Object t1, Object t2) { + public int compare(Object t1, Object t2) { Comparable c1 = (Comparable) t1; Comparable c2 = (Comparable) t2; return c1.compareTo(c2); diff --git a/src/main/java/rx/internal/operators/SingleDelayedProducer.java b/src/main/java/rx/internal/operators/SingleDelayedProducer.java new file mode 100644 index 0000000000..9405250ac5 --- /dev/null +++ b/src/main/java/rx/internal/operators/SingleDelayedProducer.java @@ -0,0 +1,87 @@ +package rx.internal.operators; + +import java.util.concurrent.atomic.AtomicInteger; + +import rx.*; + +/** + * A producer that holds a single value until it is requested and emits it followed by an onCompleted. + */ +public final class SingleDelayedProducer extends AtomicInteger implements Producer { + /** */ + private static final long serialVersionUID = 4721551710164477552L; + /** The actual child. */ + final Subscriber child; + /** The value to emit, acquired and released by compareAndSet. */ + T value; + /** State flag: request() called with positive value. */ + static final int REQUESTED = 1; + /** State flag: set() called. */ + static final int SET = 2; + /** + * Constructs a SingleDelayedProducer with the given child as output. + * @param child the subscriber to emit the value and completion events + */ + public SingleDelayedProducer(Subscriber child) { + this.child = child; + } + @Override + public void request(long n) { + if (n > 0) { + for (;;) { + int s = get(); + // if already requested + if ((s & REQUESTED) != 0) { + break; + } + int u = s | REQUESTED; + if (compareAndSet(s, u)) { + if ((s & SET) != 0) { + emit(); + } + break; + } + } + } + } + /** + * Sets the value to be emitted and emits it if there was a request. + * Should be called only once and from a single thread + * @param value the value to set and possibly emit + */ + public void set(T value) { + for (;;) { + int s = get(); + // if already set + if ((s & SET) != 0) { + break; + } + int u = s | SET; + this.value = value; + if (compareAndSet(s, u)) { + if ((s & REQUESTED) != 0) { + emit(); + } + break; + } + } + } + /** + * Emits the set value if the child is not unsubscribed and bounces back + * exceptions caught from child.onNext. + */ + void emit() { + try { + T v = value; + value = null; // do not hold onto the value + if (child.isUnsubscribed()) { + return; + } + child.onNext(v); + } catch (Throwable t) { + child.onError(t); + return; + } + child.onCompleted(); + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorToObservableListTest.java b/src/test/java/rx/internal/operators/OperatorToObservableListTest.java index 298c6f5f62..c38786b286 100644 --- a/src/test/java/rx/internal/operators/OperatorToObservableListTest.java +++ b/src/test/java/rx/internal/operators/OperatorToObservableListTest.java @@ -15,20 +15,26 @@ */ package rx.internal.operators; +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.Arrays; -import java.util.List; +import java.util.*; +import java.util.concurrent.*; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; +import rx.*; import rx.Observable; import rx.Observer; +import rx.functions.Action0; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorToObservableListTest { @@ -101,4 +107,79 @@ public void testListWithBlockingFirst() { List actual = o.toList().toBlocking().first(); Assert.assertEquals(Arrays.asList("one", "two", "three"), actual); } + @Test + public void testBackpressureHonored() { + Observable> w = Observable.just(1, 2, 3, 4, 5).toList(); + TestSubscriber> ts = new TestSubscriber>() { + @Override + public void onStart() { + requestMore(0); + } + }; + + w.subscribe(ts); + + assertTrue(ts.getOnNextEvents().isEmpty()); + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertTrue(ts.getOnCompletedEvents().isEmpty()); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Collections.singletonList(Arrays.asList(1, 2, 3, 4, 5))); + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertEquals(1, ts.getOnCompletedEvents().size()); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Collections.singletonList(Arrays.asList(1, 2, 3, 4, 5))); + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertEquals(1, ts.getOnCompletedEvents().size()); + } + @Test(timeout = 2000) + public void testAsyncRequested() { + Scheduler.Worker w = Schedulers.newThread().createWorker(); + try { + for (int i = 0; i < 1000; i++) { + if (i % 50 == 0) { + System.out.println("testAsyncRequested -> " + i); + } + PublishSubject source = PublishSubject.create(); + Observable> sorted = source.toList(); + + final CyclicBarrier cb = new CyclicBarrier(2); + final TestSubscriber> ts = new TestSubscriber>() { + @Override + public void onStart() { + requestMore(0); + } + }; + sorted.subscribe(ts); + w.schedule(new Action0() { + @Override + public void call() { + await(cb); + ts.requestMore(1); + } + }); + source.onNext(1); + await(cb); + source.onCompleted(); + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.singletonList(Arrays.asList(1))); + } + } finally { + w.unsubscribe(); + } + } + static void await(CyclicBarrier cb) { + try { + cb.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } catch (BrokenBarrierException ex) { + ex.printStackTrace(); + } + } } diff --git a/src/test/java/rx/internal/operators/OperatorToObservableSortedListTest.java b/src/test/java/rx/internal/operators/OperatorToObservableSortedListTest.java index d304e9443e..0b1d64bf87 100644 --- a/src/test/java/rx/internal/operators/OperatorToObservableSortedListTest.java +++ b/src/test/java/rx/internal/operators/OperatorToObservableSortedListTest.java @@ -15,29 +15,30 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; +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 static org.mockito.Mockito.*; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.concurrent.*; import org.junit.Test; import org.mockito.Mockito; +import rx.*; import rx.Observable; import rx.Observer; -import rx.functions.Func2; -import rx.internal.operators.OperatorToObservableSortedList; +import rx.functions.*; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorToObservableSortedListTest { @Test public void testSortedList() { Observable w = Observable.just(1, 3, 2, 5, 4); - Observable> observable = w.lift(new OperatorToObservableSortedList()); + Observable> observable = w.toSortedList(); @SuppressWarnings("unchecked") Observer> observer = mock(Observer.class); @@ -50,14 +51,14 @@ public void testSortedList() { @Test public void testSortedListWithCustomFunction() { Observable w = Observable.just(1, 3, 2, 5, 4); - Observable> observable = w.lift(new OperatorToObservableSortedList(new Func2() { + Observable> observable = w.toSortedList(new Func2() { @Override public Integer call(Integer t1, Integer t2) { return t2 - t1; } - })); + }); @SuppressWarnings("unchecked") Observer> observer = mock(Observer.class); @@ -72,4 +73,79 @@ public void testWithFollowingFirst() { Observable o = Observable.just(1, 3, 2, 5, 4); assertEquals(Arrays.asList(1, 2, 3, 4, 5), o.toSortedList().toBlocking().first()); } + @Test + public void testBackpressureHonored() { + Observable> w = Observable.just(1, 3, 2, 5, 4).toSortedList(); + TestSubscriber> ts = new TestSubscriber>() { + @Override + public void onStart() { + requestMore(0); + } + }; + + w.subscribe(ts); + + assertTrue(ts.getOnNextEvents().isEmpty()); + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertTrue(ts.getOnCompletedEvents().isEmpty()); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Collections.singletonList(Arrays.asList(1, 2, 3, 4, 5))); + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertEquals(1, ts.getOnCompletedEvents().size()); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Collections.singletonList(Arrays.asList(1, 2, 3, 4, 5))); + assertTrue(ts.getOnErrorEvents().isEmpty()); + assertEquals(1, ts.getOnCompletedEvents().size()); + } + @Test(timeout = 2000) + public void testAsyncRequested() { + Scheduler.Worker w = Schedulers.newThread().createWorker(); + try { + for (int i = 0; i < 1000; i++) { + if (i % 50 == 0) { + System.out.println("testAsyncRequested -> " + i); + } + PublishSubject source = PublishSubject.create(); + Observable> sorted = source.toSortedList(); + + final CyclicBarrier cb = new CyclicBarrier(2); + final TestSubscriber> ts = new TestSubscriber>() { + @Override + public void onStart() { + requestMore(0); + } + }; + sorted.subscribe(ts); + w.schedule(new Action0() { + @Override + public void call() { + await(cb); + ts.requestMore(1); + } + }); + source.onNext(1); + await(cb); + source.onCompleted(); + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.singletonList(Arrays.asList(1))); + } + } finally { + w.unsubscribe(); + } + } + static void await(CyclicBarrier cb) { + try { + cb.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } catch (BrokenBarrierException ex) { + ex.printStackTrace(); + } + } } From 7a6dd8dd7339ff75d585f9e6507a158ae9454615 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 21 Apr 2015 21:13:47 +1000 Subject: [PATCH 237/857] stack overflow test can hang build, simplify the tests and ensure don't hang --- .../java/rx/exceptions/ExceptionsTest.java | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/test/java/rx/exceptions/ExceptionsTest.java b/src/test/java/rx/exceptions/ExceptionsTest.java index a78b2b6af4..4148f1b9e6 100644 --- a/src/test/java/rx/exceptions/ExceptionsTest.java +++ b/src/test/java/rx/exceptions/ExceptionsTest.java @@ -18,6 +18,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.concurrent.atomic.AtomicInteger; + import org.junit.Test; import rx.Observable; @@ -39,27 +41,13 @@ public void call(Integer t1) { }); } - @Test(expected = StackOverflowError.class) - public void testStackOverflowIsThrown() { + @Test + public void testStackOverflowWouldOccur() { final PublishSubject a = PublishSubject.create(); final PublishSubject b = PublishSubject.create(); - new Observer() { - - @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - e.printStackTrace(); - } - - @Override - public void onNext(Integer args) { - System.out.println(args); - } - }; + final int MAX_STACK_DEPTH = 1000; + final AtomicInteger depth = new AtomicInteger(); + a.subscribe(new Observer() { @Override @@ -73,12 +61,11 @@ public void onError(Throwable e) { } @Override - public void onNext(Integer args) { - System.out.println(args); + public void onNext(Integer n) { + b.onNext(n + 1); } }); - b.subscribe(); - a.subscribe(new Observer() { + b.subscribe(new Observer() { @Override public void onCompleted() { @@ -91,11 +78,20 @@ public void onError(Throwable e) { } @Override - public void onNext(Integer args) { - b.onNext(args + 1); + public void onNext(Integer n) { + if (depth.get() < MAX_STACK_DEPTH) { + depth.set(Thread.currentThread().getStackTrace().length); + a.onNext(n + 1); + } } }); - b.subscribe(new Observer() { + a.onNext(1); + assertTrue(depth.get() > MAX_STACK_DEPTH); + } + + @Test(expected = StackOverflowError.class) + public void testStackOverflowErrorIsThrown() { + Observable.just(1).subscribe(new Observer() { @Override public void onCompleted() { @@ -108,11 +104,11 @@ public void onError(Throwable e) { } @Override - public void onNext(Integer args) { - a.onNext(args + 1); + public void onNext(Integer t) { + throw new StackOverflowError(); } + }); - a.onNext(1); } @Test(expected = ThreadDeath.class) From 01cddd5ee83452c769f8b9ea7e8617bea21ead02 Mon Sep 17 00:00:00 2001 From: Alex Wenckus Date: Tue, 21 Apr 2015 01:34:23 +0200 Subject: [PATCH 238/857] Fix for #2896 overlapping windows. Source was emitting t multiple times while holding queue. --- .../OperatorWindowWithObservable.java | 2 +- .../OperatorWindowWithObservableTest.java | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java b/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java index 9242d6586d..c5fec0a13d 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java @@ -119,7 +119,7 @@ public void onNext(T t) { do { drain(localQueue); if (once) { - once = true; + once = false; emitValue(t); } diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java index 8d6b9bb6a5..fd8a20fed3 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.verify; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.Test; @@ -29,6 +30,8 @@ import rx.Observable; import rx.Observer; import rx.exceptions.TestException; +import rx.functions.Func0; +import rx.observers.TestSubscriber; import rx.subjects.PublishSubject; public class OperatorWindowWithObservableTest { @@ -252,4 +255,39 @@ public void onCompleted() { verify(o, never()).onCompleted(); verify(o).onError(any(TestException.class)); } + + @Test + public void testWindowNoDuplication() { + final PublishSubject source = PublishSubject.create(); + final TestSubscriber tsw = new TestSubscriber() { + boolean once; + @Override + public void onNext(Integer t) { + if (!once) { + once = true; + source.onNext(2); + } + super.onNext(t); + } + }; + TestSubscriber> ts = new TestSubscriber>() { + @Override + public void onNext(Observable t) { + t.subscribe(tsw); + super.onNext(t); + } + }; + source.window(new Func0>() { + @Override + public Observable call() { + return Observable.never(); + } + }).subscribe(ts); + + source.onNext(1); + source.onCompleted(); + + assertEquals(1, ts.getOnNextEvents().size()); + assertEquals(Arrays.asList(1, 2), tsw.getOnNextEvents()); + } } \ No newline at end of file From 01ceedce3d99843b4bb12a021cc27d438df08f5b Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 22 Apr 2015 13:06:28 +1000 Subject: [PATCH 239/857] TakeLastQueueProducer add request overflow check --- .../operators/TakeLastQueueProducer.java | 2 +- .../operators/OperatorTakeLastTest.java | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java index 041242163d..633d28ca66 100644 --- a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java +++ b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java @@ -55,7 +55,7 @@ public void request(long n) { if (n == Long.MAX_VALUE) { _c = REQUESTED_UPDATER.getAndSet(this, Long.MAX_VALUE); } else { - _c = REQUESTED_UPDATER.getAndAdd(this, n); + _c = BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER, this, n); } if (!emittingStarted) { // we haven't started yet, so record what was requested and return diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java index c2b1ef014c..c3297db0a0 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeLastTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeLastTest.java @@ -23,7 +23,9 @@ 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; @@ -293,4 +295,32 @@ public void onNext(Integer integer) { }); assertEquals(1,count.get()); } + + @Test(timeout=10000) + public void testRequestOverflow() { + final List list = new ArrayList(); + Observable.range(1, 100).takeLast(50).subscribe(new Subscriber() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + list.add(t); + request(Long.MAX_VALUE-1); + }}); + assertEquals(50, list.size()); + } } From 1ef4d50be3da6c30715532ae0f98341ccb8c2fc3 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 22 Apr 2015 16:39:09 +1000 Subject: [PATCH 240/857] reduce gc pressure by using singleton Operators for single, distinct, distinctUntilChanged, onBackpressureBuffer, isEmpty --- src/main/java/rx/Observable.java | 34 +++++++++++++++---- .../internal/operators/OperatorDistinct.java | 17 ++++++++++ .../OperatorDistinctUntilChanged.java | 17 ++++++++++ .../OperatorOnBackpressureBuffer.java | 11 +++++- .../rx/internal/operators/OperatorSingle.java | 17 +++++++++- 5 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 2d6e48eda2..38249d44e6 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -2295,7 +2295,7 @@ public final Observable> nest() { * @see ReactiveX operators documentation: Never */ public final static Observable never() { - return new NeverObservable(); + return NeverObservable.instance(); } /** @@ -4026,7 +4026,7 @@ public final Observable dematerialize() { * @see ReactiveX operators documentation: Distinct */ public final Observable distinct() { - return lift(new OperatorDistinct(UtilityFunctions.identity())); + return lift(OperatorDistinct. instance()); } /** @@ -4064,7 +4064,7 @@ public final Observable distinct(Func1 keySelecto * @see ReactiveX operators documentation: Distinct */ public final Observable distinctUntilChanged() { - return lift(new OperatorDistinctUntilChanged(UtilityFunctions.identity())); + return lift(OperatorDistinctUntilChanged. instance()); } /** @@ -4959,8 +4959,13 @@ public final Observable ignoreElements() { * @return an Observable that emits a Boolean * @see ReactiveX operators documentation: Contains */ + @SuppressWarnings("unchecked") public final Observable isEmpty() { - return lift(new OperatorAny(UtilityFunctions.alwaysTrue(), true)); + return lift((OperatorAny) HolderAnyForEmpty.INSTANCE); + } + + private static class HolderAnyForEmpty { + static final OperatorAny INSTANCE = new OperatorAny(UtilityFunctions.alwaysTrue(), true); } /** @@ -5226,7 +5231,7 @@ public final Boolean call(T t) { * @see ReactiveX operators documentation: backpressure operators */ public final Observable onBackpressureBuffer() { - return lift(new OperatorOnBackpressureBuffer()); + return lift(OperatorOnBackpressureBuffer. instance()); } /** @@ -6709,7 +6714,7 @@ public final Observable share() { * @see ReactiveX operators documentation: First */ public final Observable single() { - return lift(new OperatorSingle()); + return lift(OperatorSingle. instance()); } /** @@ -9276,7 +9281,22 @@ public final Observable zipWith(Observable other, Func2 * the type of item (not) emitted by the Observable */ private static class NeverObservable extends Observable { - public NeverObservable() { + + private static class Holder { + static final NeverObservable INSTANCE = new NeverObservable(); + } + + /** + * Returns a singleton instance of NeverObservble (cast to the generic type). + * + * @return + */ + @SuppressWarnings("unchecked") + static NeverObservable instance() { + return (NeverObservable) Holder.INSTANCE; + } + + NeverObservable() { super(new OnSubscribe() { @Override diff --git a/src/main/java/rx/internal/operators/OperatorDistinct.java b/src/main/java/rx/internal/operators/OperatorDistinct.java index f5cdba5bd5..b71738b7c1 100644 --- a/src/main/java/rx/internal/operators/OperatorDistinct.java +++ b/src/main/java/rx/internal/operators/OperatorDistinct.java @@ -17,9 +17,11 @@ import java.util.HashSet; import java.util.Set; + import rx.Observable.Operator; import rx.Subscriber; import rx.functions.Func1; +import rx.internal.util.UtilityFunctions; /** * Returns an Observable that emits all distinct items emitted by the source. @@ -29,6 +31,21 @@ */ public final class OperatorDistinct implements Operator { final Func1 keySelector; + + private static class Holder { + static final OperatorDistinct INSTANCE = new OperatorDistinct(UtilityFunctions.identity()); + } + + /** + * Returns a singleton instance of OperatorDistinct that was built using + * the identity function for comparison (new OperatorDistinct(UtilityFunctions.identity())). + * + * @return Operator that emits distinct values only (regardless of order) using the identity function for comparison + */ + @SuppressWarnings("unchecked") + public static OperatorDistinct instance() { + return (OperatorDistinct) Holder.INSTANCE; + } public OperatorDistinct(Func1 keySelector) { this.keySelector = keySelector; diff --git a/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java b/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java index dd59055776..275e33d0db 100644 --- a/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java +++ b/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java @@ -18,6 +18,7 @@ import rx.Observable.Operator; import rx.Subscriber; import rx.functions.Func1; +import rx.internal.util.UtilityFunctions; /** * Returns an Observable that emits all sequentially distinct items emitted by the source. @@ -26,6 +27,22 @@ */ public final class OperatorDistinctUntilChanged implements Operator { final Func1 keySelector; + + private static class Holder { + static final OperatorDistinctUntilChanged INSTANCE = new OperatorDistinctUntilChanged(UtilityFunctions.identity()); + } + + + /** + * Returns a singleton instance of OperatorDistinctUntilChanged that was built using + * the identity function for comparison (new OperatorDistinctUntilChanged(UtilityFunctions.identity())). + * + * @return Operator that emits sequentially distinct values only using the identity function for comparison + */ + @SuppressWarnings("unchecked") + public static OperatorDistinctUntilChanged instance() { + return (OperatorDistinctUntilChanged) Holder.INSTANCE; + } public OperatorDistinctUntilChanged(Func1 keySelector) { this.keySelector = keySelector; diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index e35c489d5c..cb39a53ef7 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -31,7 +31,16 @@ public class OperatorOnBackpressureBuffer implements Operator { private final Long capacity; private final Action0 onOverflow; - public OperatorOnBackpressureBuffer() { + private static class Holder { + static final OperatorOnBackpressureBuffer INSTANCE = new OperatorOnBackpressureBuffer(); + } + + @SuppressWarnings("unchecked") + public static OperatorOnBackpressureBuffer instance() { + return (OperatorOnBackpressureBuffer) Holder.INSTANCE; + } + + private OperatorOnBackpressureBuffer() { this.capacity = null; this.onOverflow = null; } diff --git a/src/main/java/rx/internal/operators/OperatorSingle.java b/src/main/java/rx/internal/operators/OperatorSingle.java index 7a400dd9be..53afca58c8 100644 --- a/src/main/java/rx/internal/operators/OperatorSingle.java +++ b/src/main/java/rx/internal/operators/OperatorSingle.java @@ -32,7 +32,22 @@ public final class OperatorSingle implements Operator { private final boolean hasDefaultValue; private final T defaultValue; - public OperatorSingle() { + private static class Holder { + final static OperatorSingle INSTANCE = new OperatorSingle(); + } + + /** + * Returns a singleton instance of OperatorSingle (if the stream is empty or has + * more than one element an error will be emitted) that is cast to the generic type. + * + * @return a singleton instance of an Operator that will emit a single value only unless the stream has zero or more than one element in which case it will emit an error. + */ + @SuppressWarnings("unchecked") + public static OperatorSingle instance() { + return (OperatorSingle) Holder.INSTANCE; + } + + private OperatorSingle() { this(false, null); } From 53de3f2bb0339222af7cb887d53ba297a3bf8869 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 23 Apr 2015 11:05:23 +0200 Subject: [PATCH 241/857] Fix the drainer to check if the queue is empty before quitting. --- .../util/RxRingBufferWithoutUnsafeTest.java | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/test/java/rx/internal/util/RxRingBufferWithoutUnsafeTest.java b/src/test/java/rx/internal/util/RxRingBufferWithoutUnsafeTest.java index 39fc041fc8..3c4409688a 100644 --- a/src/test/java/rx/internal/util/RxRingBufferWithoutUnsafeTest.java +++ b/src/test/java/rx/internal/util/RxRingBufferWithoutUnsafeTest.java @@ -17,13 +17,13 @@ import static org.junit.Assert.assertEquals; +import java.util.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; -import rx.Producer; -import rx.Scheduler; +import rx.*; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; import rx.observers.TestSubscriber; @@ -36,18 +36,25 @@ protected RxRingBuffer createRingBuffer() { return new RxRingBuffer(); } + @Test(timeout = 20000) + public void testConcurrencyLoop() throws InterruptedException { + for (int i = 0; i < 50; i++) { + testConcurrency(); + } + } + /** * Single producer, 2 consumers. The request() ensures it gets scheduled back on the same Producer thread. */ - @Test + @Test(timeout = 10000) public void testConcurrency() throws InterruptedException { final RxRingBuffer b = createRingBuffer(); - final CountDownLatch emitLatch = new CountDownLatch(255); - final CountDownLatch drainLatch = new CountDownLatch(2); + final CountDownLatch emitLatch = new CountDownLatch(127); + int drainers = 3; + final CountDownLatch drainLatch = new CountDownLatch(drainers); final Scheduler.Worker w1 = Schedulers.newThread().createWorker(); - Scheduler.Worker w2 = Schedulers.newThread().createWorker(); - Scheduler.Worker w3 = Schedulers.newThread().createWorker(); + List drainerWorkers = new ArrayList(); final AtomicInteger emit = new AtomicInteger(); final AtomicInteger poll = new AtomicInteger(); @@ -110,7 +117,12 @@ public void call() { ts.requestMore(emitted); emitted = 0; } else { - if (emitLatch.getCount() == 0) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + // ignored + } + if (emitLatch.getCount() == 0 && b.isEmpty()) { // this works with SynchronizedQueue, if changing to a non-blocking Queue // then this will likely need to change like the SpmcTest version drainLatch.countDown(); @@ -124,14 +136,18 @@ public void call() { }; - w2.schedule(drainer); - w3.schedule(drainer); + for (int i = 0; i < drainers; i++) { + Scheduler.Worker w = Schedulers.newThread().createWorker(); + w.schedule(drainer); + drainerWorkers.add(w); + } emitLatch.await(); drainLatch.await(); - w2.unsubscribe(); - w3.unsubscribe(); + for (Scheduler.Worker w : drainerWorkers) { + w.unsubscribe(); + } w1.unsubscribe(); // put this one last as unsubscribing from it can cause Exceptions to be throw in w2/w3 System.out.println("emit: " + emit.get() + " poll: " + poll.get()); From 9759e6356be76726f5e2e3e3ac5ab7c5933edcb5 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 23 Apr 2015 15:42:08 +0200 Subject: [PATCH 242/857] OperatorPublish benchmark --- .../rx/operators/OperatorPublishPerf.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/perf/java/rx/operators/OperatorPublishPerf.java diff --git a/src/perf/java/rx/operators/OperatorPublishPerf.java b/src/perf/java/rx/operators/OperatorPublishPerf.java new file mode 100644 index 0000000000..0e9a1d65c5 --- /dev/null +++ b/src/perf/java/rx/operators/OperatorPublishPerf.java @@ -0,0 +1,159 @@ +/** + * 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.*; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import rx.*; +import rx.Observable; +import rx.functions.Action0; +import rx.observables.ConnectableObservable; +import rx.schedulers.Schedulers; + +/** + * Benchmark typical atomic operations on volatile fields and AtomicXYZ classes. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*OperatorPublishPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class OperatorPublishPerf { + static final class SharedLatchObserver extends Subscriber { + final CountDownLatch cdl; + final int batchFrequency; + final Blackhole bh; + int received; + public SharedLatchObserver(CountDownLatch cdl, int batchFrequency, Blackhole bh) { + this.cdl = cdl; + this.batchFrequency = batchFrequency; + this.bh = bh; + } + @Override + public void onStart() { + request(batchFrequency); + } + @Override + public void onNext(Integer t) { + if (bh != null) { + bh.consume(t); + } + if (++received == batchFrequency) { + received = 0; + request(batchFrequency); + } + } + @Override + public void onError(Throwable e) { + e.printStackTrace(); + cdl.countDown(); + } + @Override + public void onCompleted() { + cdl.countDown(); + } + } + + + /** How long the range should be. */ + @Param({"1", "1000", "1000000"}) + private int size; + /** Should children use observeOn? */ + @Param({"false", "true"}) + private boolean async; + /** Number of child subscribers. */ + @Param({"0", "1", "2", "3", "4", "5"}) + private int childCount; + /** How often the child subscribers should re-request. */ + @Param({"1", "2", "4", "8", "16", "32", "64"}) + private int batchFrequency; + + private ConnectableObservable source; + private Observable observable; + private CyclicBarrier sourceDone; + @Setup + public void setup() { + List list = new ArrayList(); + for (int i = 0; i < size; i++) { + list.add(i); + } + Observable src = Observable.from(list); + if (childCount == 0) { + sourceDone = new CyclicBarrier(2); + src = src + // for childCount == 0, make sure we measure how fast the source is depleted + .doOnCompleted(new Action0() { + @Override + public void call() { + try { + sourceDone.await(2, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + // ignored + } catch (BrokenBarrierException ex) { + // ignored + } catch (TimeoutException ex) { + // ignored + } + } + }); + } + source = src.publish(); + observable = async ? source.observeOn(Schedulers.computation()) : source; + } + + @Benchmark + public void benchmark(Blackhole bh) throws InterruptedException, + TimeoutException, BrokenBarrierException { + CountDownLatch completion = null; + int cc = childCount; + + if (cc > 0) { + completion = new CountDownLatch(cc); + Observable o = observable; + for (int i = 0; i < childCount; i++) { + o.subscribe(new SharedLatchObserver(completion, batchFrequency, bh)); + } + } + + Subscription s = source.connect(); + + if (cc == 0) { + sourceDone.await(2, TimeUnit.SECONDS); + } + if (completion != null && !completion.await(2, TimeUnit.SECONDS)) { + throw new RuntimeException("Source hung!"); + } + s.unsubscribe(); + } +// public static void main(String[] args) throws Exception { +// OperatorPublishPerf o = new OperatorPublishPerf(); +// o.async = true; +// o.batchFrequency = 1; +// o.childCount = 1; +// o.size = 1000000; +// o.setup(); +// for (int j = 0; j < 1000; j++) { +// o.benchmark(null); +// } +// } +} From bf12d3279361a9fb9ffba2bce95f36b7931a6fe2 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 23 Apr 2015 16:17:55 +0200 Subject: [PATCH 243/857] OperatorPublish full rewrite with comments + its perf fix --- .../internal/operators/OperatorPublish.java | 945 ++++++++++++------ .../rx/operators/OperatorPublishPerf.java | 46 +- .../operators/OperatorPublishTest.java | 162 ++- 3 files changed, 827 insertions(+), 326 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 193bdba6c4..0fca3d6c64 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -15,367 +15,736 @@ */ package rx.internal.operators; -import java.util.*; +import java.util.Queue; import java.util.concurrent.atomic.*; import rx.*; -import rx.Observable; -import rx.exceptions.*; +import rx.exceptions.MissingBackpressureException; import rx.functions.*; -import rx.internal.util.RxRingBuffer; +import rx.internal.util.*; +import rx.internal.util.unsafe.*; import rx.observables.ConnectableObservable; import rx.subscriptions.Subscriptions; -public class OperatorPublish extends ConnectableObservable { +/** + * A connectable observable which shares an underlying source and dispatches source values to subscribers in a backpressure-aware + * manner. + * @param the value type + */ +public final class OperatorPublish extends ConnectableObservable { + /** The source observable. */ final Observable source; - private final RequestHandler requestHandler; + /** Holds the current subscriber that is, will be or just was subscribed to the source observable. */ + final AtomicReference> current; + /** + * Creates a OperatorPublish instance to publish values of the given source observable. + * @param source the source observable + * @return the connectable observable + */ public static ConnectableObservable create(Observable source) { - return new OperatorPublish(source); + // the current connection to source needs to be shared between the operator and its onSubscribe call + final AtomicReference> curr = new AtomicReference>(); + OnSubscribe onSubscribe = new OnSubscribe() { + @Override + public void call(Subscriber child) { + // concurrent connection/disconnection may change the state, + // we loop to be atomic while the child subscribes + for (;;) { + // get the current subscriber-to-source + PublishSubscriber r = curr.get(); + // if there isn't one or it is unsubscribed + if (r == null || r.isUnsubscribed()) { + // create a new subscriber to source + PublishSubscriber u = new PublishSubscriber(curr); + // perform extra initialization to avoid 'this' to escape during construction + u.init(); + // let's try setting it as the current subscriber-to-source + if (!curr.compareAndSet(r, u)) { + // didn't work, maybe someone else did it or the current subscriber + // to source has just finished + continue; + } + // we won, let's use it going onwards + r = u; + } + + // create the backpressure-managing producer for this child + InnerProducer inner = new InnerProducer(r, child); + /* + * Try adding it to the current subscriber-to-source, add is atomic in respect + * to other adds and the termination of the subscriber-to-source. + */ + if (!r.add(inner)) { + /* + * The current PublishSubscriber has been terminated, try with a newer one. + */ + continue; + /* + * Note: although technically corrent, concurrent disconnects can cause + * unexpected behavior such as child subscribers never receiving anything + * (unless connected again). An alternative approach, similar to + * PublishSubject would be to immediately terminate such child + * subscribers as well: + * + * Object term = r.terminalEvent; + * if (r.nl.isCompleted(term)) { + * child.onCompleted(); + * } else { + * child.onError(r.nl.getError(term)); + * } + * return; + * + * The original concurrent behavior was non-deterministic in this regard as well. + * Allowing this behavior, however, may introduce another unexpected behavior: + * after disconnecting a previous connection, one might not be able to prepare + * a new connection right after a previous termination by subscribing new child + * subscribers asynchronously before a connect call. + */ + } + // the producer has been registered with the current subscriber-to-source so + // at least it will receive the next terminal event + child.add(inner); + // setting the producer will trigger the first request to be considered by + // the subscriber-to-source. + child.setProducer(inner); + break; + } + } + }; + return new OperatorPublish(onSubscribe, source, curr); } - public static Observable create(final Observable source, final Func1, ? extends Observable> selector) { - return Observable.create(new OnSubscribe() { - + public static Observable create(final Observable source, + final Func1, ? extends Observable> selector) { + return create(new OnSubscribe() { @Override public void call(final Subscriber child) { - OperatorPublish op = new OperatorPublish(source); + ConnectableObservable op = create(source); + selector.call(op).unsafeSubscribe(child); + op.connect(new Action1() { - @Override - public void call(Subscription sub) { - child.add(sub); + public void call(Subscription t1) { + child.add(t1); } - }); } - }); } - private OperatorPublish(Observable source) { - this(source, new Object(), new RequestHandler()); - } - - private OperatorPublish(Observable source, final Object guard, final RequestHandler requestHandler) { - super(new OnSubscribe() { - @Override - public void call(final Subscriber subscriber) { - subscriber.setProducer(new Producer() { - - @Override - public void request(long n) { - requestHandler.requestFromChildSubscriber(subscriber, n); - } - - }); - subscriber.add(Subscriptions.create(new Action0() { - - @Override - public void call() { - requestHandler.state.removeSubscriber(subscriber); - } - - })); - } - }); + private OperatorPublish(OnSubscribe onSubscribe, Observable source, + final AtomicReference> current) { + super(onSubscribe); this.source = source; - this.requestHandler = requestHandler; + this.current = current; } @Override public void connect(Action1 connection) { - // each time we connect we create a new Subscription - boolean shouldSubscribe = false; + boolean doConnect = false; + PublishSubscriber ps; + // we loop because concurrent connect/disconnect and termination may change the state + for (;;) { + // retrieve the current subscriber-to-source instance + ps = current.get(); + // if there is none yet or the current has unsubscribed + if (ps == null || ps.isUnsubscribed()) { + // create a new subscriber-to-source + PublishSubscriber u = new PublishSubscriber(current); + // initialize out the constructor to avoid 'this' to escape + u.init(); + // try setting it as the current subscriber-to-source + if (!current.compareAndSet(ps, u)) { + // did not work, perhaps a new subscriber arrived + // and created a new subscriber-to-source as well, retry + continue; + } + ps = u; + } + // if connect() was called concurrently, only one of them should actually + // connect to the source + doConnect = !ps.shouldConnect.get() && ps.shouldConnect.compareAndSet(false, true); + break; + } + /* + * Notify the callback that we have a (new) connection which it can unsubscribe + * but since ps is unique to a connection, multiple calls to connect() will return the + * same Subscription and even if there was a connect-disconnect-connect pair, the older + * references won't disconnect the newer connection. + * Synchronous source consumers have the opportunity to disconnect via unsubscribe on the + * Subscription as unsafeSubscribe may never return in its own. + * + * Note however, that asynchronously disconnecting a running source might leave + * child-subscribers without any terminal event; PublishSubject does not have this + * issue because the unsubscription was always triggered by the child-subscribers + * themselves. + */ + connection.call(ps); + if (doConnect) { + source.unsafeSubscribe(ps); + } + } + + @SuppressWarnings("rawtypes") + static final class PublishSubscriber extends Subscriber implements Subscription { + /** Holds notifications from upstream. */ + final Queue queue; + /** The notification-lite factory. */ + final NotificationLite nl; + /** Holds onto the current connected PublishSubscriber. */ + final AtomicReference> current; + /** Contains either an onCompleted or an onError token from upstream. */ + volatile Object terminalEvent; + + /** Indicates an empty array of inner producers. */ + static final InnerProducer[] EMPTY = new InnerProducer[0]; + /** Indicates a terminated PublishSubscriber. */ + static final InnerProducer[] TERMINATED = new InnerProducer[0]; + + /** Tracks the subscribed producers. */ + final AtomicReference producers; + /** + * Atomically changed from false to true by connect to make sure the + * connection is only performed by one thread. + */ + final AtomicBoolean shouldConnect; + + /** Guarded by this. */ + boolean emitting; + /** Guarded by this. */ + boolean missed; - // subscription is the state of whether we are connected or not - OriginSubscriber origin = requestHandler.state.getOrigin(); - if (origin == null) { - shouldSubscribe = true; - requestHandler.state.setOrigin(new OriginSubscriber(requestHandler)); + public PublishSubscriber(AtomicReference> current) { + this.queue = UnsafeAccess.isUnsafeAvailable() + ? new SpscArrayQueue(RxRingBuffer.SIZE) + : new SynchronizedQueue(RxRingBuffer.SIZE); + + this.nl = NotificationLite.instance(); + this.producers = new AtomicReference(EMPTY); + this.current = current; + this.shouldConnect = new AtomicBoolean(); } - - // in the lock above we determined we should subscribe, do it now outside the lock - if (shouldSubscribe) { - // register a subscription that will shut this down - connection.call(Subscriptions.create(new Action0() { + + /** Should be called after the constructor finished to setup nulling-out the current reference. */ + void init() { + add(Subscriptions.create(new Action0() { @Override public void call() { - OriginSubscriber s = requestHandler.state.getOrigin(); - requestHandler.state.setOrigin(null); - if (s != null) { - s.unsubscribe(); - } + PublishSubscriber.this.producers.getAndSet(TERMINATED); + current.compareAndSet(PublishSubscriber.this, null); + // we don't care if it fails because it means the current has + // been replaced in the meantime } })); - - // now that everything is hooked up let's subscribe - // as long as the subscription is not null (which can happen if already unsubscribed) - OriginSubscriber os = requestHandler.state.getOrigin(); - if (os != null) { - source.unsafeSubscribe(os); - } } - } - - private static class OriginSubscriber extends Subscriber { - - private final RequestHandler requestHandler; - private final AtomicLong originOutstanding = new AtomicLong(); - private final long THRESHOLD = RxRingBuffer.SIZE / 4; - private final RxRingBuffer buffer = RxRingBuffer.getSpmcInstance(); - - OriginSubscriber(RequestHandler requestHandler) { - this.requestHandler = requestHandler; - add(buffer); - } - + @Override public void onStart() { - requestMore(RxRingBuffer.SIZE); + // since subscribers may have different amount of requests, we try to + // optimize by buffering values up-front and replaying it on individual demand + request(RxRingBuffer.SIZE); } - - private void requestMore(long r) { - originOutstanding.addAndGet(r); - request(r); - } - @Override - public void onCompleted() { - try { - requestHandler.emit(requestHandler.notifier.completed()); - } catch (MissingBackpressureException e) { - onError(e); + public void onNext(T t) { + // we expect upstream to honor backpressure requests + // nl is required because JCTools queue doesn't accept nulls. + if (!queue.offer(nl.next(t))) { + onError(new MissingBackpressureException()); + } else { + // since many things can happen concurrently, we have a common dispatch + // loop to act on the current state serially + dispatch(); } } - @Override public void onError(Throwable e) { - List errors = null; - for (Subscriber subscriber : requestHandler.state.getSubscribers()) { - try { - subscriber.onError(e); - } catch (Throwable e2) { - if (errors == null) { - errors = new ArrayList(); - } - errors.add(e2); - } + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (terminalEvent == null) { + terminalEvent = nl.error(e); + // since many things can happen concurrently, we have a common dispatch + // loop to act on the current state serially + dispatch(); } - Exceptions.throwIfAny(errors); } - @Override - public void onNext(T t) { - try { - requestHandler.emit(requestHandler.notifier.next(t)); - } catch (MissingBackpressureException e) { - onError(e); + public void onCompleted() { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (terminalEvent == null) { + terminalEvent = nl.completed(); + // since many things can happen concurrently, we have a common dispatch loop + // to act on the current state serially + dispatch(); } } - - } - - /** - * Synchronized mutable state. - * - * benjchristensen => I have not figured out a non-blocking approach to this that doesn't involve massive object allocation overhead - * with a complicated state machine so I'm sticking with mutex locks and just trying to make sure the work done while holding the - * lock is small (such as never emitting data). - * - * This does however mean we can't rely on a reference to State being consistent. For example, it can end up with a null OriginSubscriber. - * - * @param - */ - private static class State { - private long outstandingRequests = -1; - private OriginSubscriber origin; - // using AtomicLong to simplify mutating it, not for thread-safety since we're synchronizing access to this class - // using LinkedHashMap so the order of Subscribers having onNext invoked is deterministic (same each time the code is run) - private final Map, AtomicLong> ss = new LinkedHashMap, AtomicLong>(); - @SuppressWarnings("unchecked") - private Subscriber[] subscribers = new Subscriber[0]; - - public synchronized OriginSubscriber getOrigin() { - return origin; - } - - public synchronized void setOrigin(OriginSubscriber o) { - this.origin = o; - } - - public synchronized boolean canEmitWithDecrement() { - if (outstandingRequests > 0) { - outstandingRequests--; - return true; + + /** + * Atomically try adding a new InnerProducer to this Subscriber or return false if this + * Subscriber was terminated. + * @param producer the producer to add + * @return true if succeeded, false otherwise + */ + boolean add(InnerProducer producer) { + if (producer == null) { + throw new NullPointerException(); + } + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // get the current producer array + InnerProducer[] c = producers.get(); + // if this subscriber-to-source reached a terminal state by receiving + // an onError or onCompleted, just refuse to add the new producer + if (c == TERMINATED) { + return false; + } + // we perform a copy-on-write logic + int len = c.length; + InnerProducer[] u = new InnerProducer[len + 1]; + System.arraycopy(c, 0, u, 0, len); + u[len] = producer; + // try setting the producers array + if (producers.compareAndSet(c, u)) { + return true; + } + // if failed, some other operation succeded (another add, remove or termination) + // so retry } - return false; - } - - public synchronized boolean hasNoSubscriber() { - return subscribers.length == 0; - } - - public synchronized void incrementOutstandingAfterFailedEmit() { - outstandingRequests++; - } - - public synchronized Subscriber[] getSubscribers() { - return subscribers; } - + /** - * @return long outstandingRequests + * Atomically removes the given producer from the producers array. + * @param producer the producer to remove */ - public synchronized long requestFromSubscriber(Subscriber subscriber, long request) { - Map, AtomicLong> subs = ss; - AtomicLong r = subs.get(subscriber); - if (r == null) { - subs.put(subscriber, new AtomicLong(request)); - } else { - do { - long current = r.get(); - if (current == Long.MAX_VALUE) { + void remove(InnerProducer producer) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // let's read the current producers array + InnerProducer[] c = producers.get(); + // if it is either empty or terminated, there is nothing to remove so we quit + if (c == EMPTY || c == TERMINATED) { + return; + } + // let's find the supplied producer in the array + // although this is O(n), we don't expect too many child subscribers in general + int j = -1; + int len = c.length; + for (int i = 0; i < len; i++) { + if (c[i].equals(producer)) { + j = i; break; } - long u = current + request; - if (u < 0) { - u = Long.MAX_VALUE; + } + // we didn't find it so just quit + if (j < 0) { + return; + } + // we do copy-on-write logic here + InnerProducer[] u; + // we don't create a new empty array if producer was the single inhabitant + // but rather reuse an empty array + if (len == 1) { + u = EMPTY; + } else { + // otherwise, create a new array one less in size + u = new InnerProducer[len - 1]; + // copy elements being before the given producer + System.arraycopy(c, 0, u, 0, j); + // copy elements being after the given producer + System.arraycopy(c, j + 1, u, j, len - j - 1); + } + // try setting this new array as + if (producers.compareAndSet(c, u)) { + return; + } + // if we failed, it means something else happened + // (a concurrent add/remove or termination), we need to retry + } + } + + /** + * Perform termination actions in case the source has terminated in some way and + * the queue has also become empty. + * @param term the terminal event (a NotificationLite.error or completed) + * @param empty set to true if the queue is empty + * @return true if there is indeed a terminal condition + */ + boolean checkTerminated(Object term, boolean empty) { + // first of all, check if there is actually a terminal event + if (term != null) { + // is it a completion event (impl. note, this is much cheaper than checking for isError) + if (nl.isCompleted(term)) { + // but we also need to have an empty queue + if (empty) { + // this will prevent OnSubscribe spinning on a terminated but + // not yet unsubscribed PublishSubscriber + current.compareAndSet(this, null); + try { + /* + * This will swap in a terminated array so add() in OnSubscribe will reject + * child subscribers to associate themselves with a terminated and thus + * never again emitting chain. + * + * Since we atomically change the contents of 'producers' only one + * operation wins at a time. If an add() wins before this getAndSet, + * its value will be part of the returned array by getAndSet and thus + * will receive the terminal notification. Otherwise, if getAndSet wins, + * add() will refuse to add the child producer and will trigger the + * creation of subscriber-to-source. + */ + for (InnerProducer ip : producers.getAndSet(TERMINATED)) { + ip.child.onCompleted(); + } + } finally { + // we explicitely unsubscribe/disconnect from the upstream + // after we sent out the terminal event to child subscribers + unsubscribe(); + } + // indicate we reached the terminal state + return true; } - if (r.compareAndSet(current, u)) { - break; + } else { + Throwable t = nl.getError(term); + // this will prevent OnSubscribe spinning on a terminated + // but not yet unsubscribed PublishSubscriber + current.compareAndSet(this, null); + try { + // this will swap in a terminated array so add() in OnSubscribe will reject + // child subscribers to associate themselves with a terminated and thus + // never again emitting chain + for (InnerProducer ip : producers.getAndSet(TERMINATED)) { + ip.child.onError(t); + } + } finally { + // we explicitely unsubscribe/disconnect from the upstream + // after we sent out the terminal event to child subscribers + unsubscribe(); } - } while (true); + // indicate we reached the terminal state + return true; + } } - - return resetAfterSubscriberUpdate(subs); - } - - public synchronized void removeSubscriber(Subscriber subscriber) { - Map, AtomicLong> subs = ss; - subs.remove(subscriber); - resetAfterSubscriberUpdate(subs); + // there is still work to be done + return false; } - - @SuppressWarnings("unchecked") - private long resetAfterSubscriberUpdate(Map, AtomicLong> subs) { - Subscriber[] subscriberArray = new Subscriber[subs.size()]; - int i = 0; - long lowest = -1; - for (Map.Entry, AtomicLong> e : subs.entrySet()) { - subscriberArray[i++] = e.getKey(); - AtomicLong l = e.getValue(); - long c = l.get(); - if (lowest == -1 || c < lowest) { - lowest = c; + + /** + * The common serialization point of events arriving from upstream and child-subscribers + * requesting more. + */ + void dispatch() { + // standard construct of emitter loop (blocking) + // if there is an emission going on, indicate that more work needs to be done + // the exact nature of this work needs to be determined from other data structures + synchronized (this) { + if (emitting) { + missed = true; + return; } + // there was no emission going on, we won and will start emitting + emitting = true; + missed = false; } - this.subscribers = subscriberArray; /* - * when receiving a request from a subscriber we reset 'outstanding' to the lowest of all subscribers + * In case an exception is thrown in the loop, we need to set emitting back to false + * on the way out (the exception will propagate up) so if it bounces back and + * onError is called, its dispatch() call will have the opportunity to emit it. + * However, if we want to exit regularly, we will set the emitting to false (+ other operations) + * atomically so we want to prevent the finally part to accidentally unlock some other + * emissions happening between the two synchronized blocks. */ - outstandingRequests = lowest; - return lowest; + boolean skipFinal = false; + try { + for (;;) { + /* + * See if the queue is empty; since we need this information multiple + * times later on, we read it one. + * Although the queue can become non-empty in the mean time, we will + * detect it through the missing flag and will do another iteration. + */ + boolean empty = queue.isEmpty(); + // if the queue is empty and the terminal event was received, quit + // and don't bother restoring emitting to false: no further activity is + // possible at this point + if (checkTerminated(terminalEvent, empty)) { + skipFinal = true; + return; + } + + // We have elements queued. Note that due to the serialization nature of dispatch() + // this loop is the only one which can turn a non-empty queue into an empty one + // and as such, no need to ask the queue itself again for that. + if (!empty) { + // We take a snapshot of the current child-subscribers. + // Concurrent subscribers may miss this iteration, but it is to be expected + @SuppressWarnings("unchecked") + InnerProducer[] ps = producers.get(); + + int len = ps.length; + // Let's assume everyone requested the maximum value. + long maxRequested = Long.MAX_VALUE; + // count how many have triggered unsubscription + int unsubscribed = 0; + + // Now find the minimum amount each child-subscriber requested + // since we can only emit that much to all of them without violating + // backpressure constraints + for (InnerProducer ip : ps) { + long r = ip.get(); + // if there is one child subscriber that hasn't requested yet + // we can't emit anything to anyone + if (r >= 0L) { + maxRequested = Math.min(maxRequested, r); + } else + // unsubscription is indicated by a special value + if (r == InnerProducer.UNSUBSCRIBED) { + unsubscribed++; + } + // we ignore those with NOT_REQUESTED as if they aren't even there + } + + // it may happen everyone has unsubscribed between here and producers.get() + // or we have no subscribers at all to begin with + if (len == unsubscribed) { + // so let's consume a value from the queue + Object v = queue.poll(); + // or terminate if there was a terminal event and the queue is empty + if (checkTerminated(terminalEvent, v == null)) { + skipFinal = true; + return; + } + // otherwise, just ask for a new value + request(1); + // and retry emitting to potential new child-subscribers + continue; + } + // if we get here, it means there are non-unsubscribed child-subscribers + // and we count the number of emitted values because the queue + // may contain less than requested + int d = 0; + while (d < maxRequested) { + Object v = queue.poll(); + empty = v == null; + // let's check if there is a terminal event and the queue became empty just now + if (checkTerminated(terminalEvent, empty)) { + skipFinal = true; + return; + } + // the queue is empty but we aren't terminated yet, finish this emission loop + if (empty) { + break; + } + // we need to unwrap potential nulls + T value = nl.getValue(v); + // let's emit this value to all child subscribers + for (InnerProducer ip : ps) { + // if ip.get() is negative, the child has either unsubscribed in the + // meantime or hasn't requested anything yet + // this eager behavior will skip unsubscribed children in case + // multiple values are available in the queue + if (ip.get() > 0L) { + try { + ip.child.onNext(value); + } catch (Throwable t) { + // we bounce back exceptions and kick out the child subscriber + ip.unsubscribe(); + ip.child.onError(t); + continue; + } + // indicate this child has received 1 element + ip.produced(1); + } + } + // indicate we emitted one element + d++; + } + + // if we did emit at least one element, request more to replenish the queue + if (d > 0) { + request(d); + } + // if we have requests but not an empty queue after emission + // let's try again to see if more requests/child-subscribers are + // ready to receive more + if (maxRequested != 0L && !empty) { + continue; + } + } + + // we did what we could: either the queue is empty or child subscribers + // haven't requested more (or both), let's try to finish dispatching + synchronized (this) { + // since missed is changed atomically, if we see it as true + // it means some state has changed and we need to loop again + // and handle that case + if (!missed) { + // but if no missed dispatch happened, let's stop emitting + emitting = false; + // and skip the emitting = false in the finally block as well + skipFinal = true; + return; + } + // we acknowledge the missed changes so far + missed = false; + } + } + } finally { + // unless returned cleanly (i.e., some method above threw) + if (!skipFinal) { + // we stop emitting so the error can propagate back down through onError + synchronized (this) { + emitting = false; + } + } + } } } - - private static class RequestHandler { - private final NotificationLite notifier = NotificationLite.instance(); + /** + * A Producer and Subscription that manages the request and unsubscription state of a + * child subscriber in thread-safe manner. + * We use AtomicLong as a base class to save on extra allocation of an AtomicLong and also + * save the overhead of the AtomicIntegerFieldUpdater. + * @param the value type + */ + static final class InnerProducer extends AtomicLong implements Producer, Subscription { + /** */ + private static final long serialVersionUID = -4453897557930727610L; + /** + * The parent subscriber-to-source used to allow removing the child in case of + * child unsubscription. + */ + final PublishSubscriber parent; + /** The actual child subscriber. */ + final Subscriber child; + /** + * Indicates this child has been unsubscribed: the state is swapped in atomically and + * will prevent the dispatch() to emit (too many) values to a terminated child subscriber. + */ + static final long UNSUBSCRIBED = Long.MIN_VALUE; + /** + * Indicates this child has not yet requested any value. We pretend we don't + * see such child subscribers in dispatch() to allow other child subscribers who + * have requested to make progress. In a concurrent subscription scennario, + * one can't be sure when a subscription happens exactly so this virtual shift + * should not cause any problems. + */ + static final long NOT_REQUESTED = Long.MIN_VALUE / 2; - private final State state = new State(); - @SuppressWarnings("unused") - volatile long wip; - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater WIP = AtomicLongFieldUpdater.newUpdater(RequestHandler.class, "wip"); - - public void requestFromChildSubscriber(Subscriber subscriber, long request) { - state.requestFromSubscriber(subscriber, request); - OriginSubscriber originSubscriber = state.getOrigin(); - if(originSubscriber != null) { - drainQueue(originSubscriber); - } + public InnerProducer(PublishSubscriber parent, Subscriber child) { + this.parent = parent; + this.child = child; + this.lazySet(NOT_REQUESTED); } - - public void emit(Object t) throws MissingBackpressureException { - OriginSubscriber originSubscriber = state.getOrigin(); - if(originSubscriber == null) { - // unsubscribed so break ... we are done + + @Override + public void request(long n) { + // ignore negative requests + if (n < 0) { return; } - if (notifier.isCompleted(t)) { - originSubscriber.buffer.onCompleted(); - } else { - originSubscriber.buffer.onNext(notifier.getValue(t)); + // In general, RxJava doesn't prevent concurrent requests (with each other or with + // an unsubscribe) so we need a CAS-loop, but we need to handle + // request overflow and unsubscribed/not requested state as well. + for (;;) { + // get the current request amount + long r = get(); + // if child called unsubscribe() do nothing + if (r == UNSUBSCRIBED) { + return; + } + // ignore zero requests except any first that sets in zero + if (r >= 0L && n == 0) { + return; + } + long u; + // if this child has not requested yet + if (r == NOT_REQUESTED) { + // let the new request value this (no overflow check needed) + u = n; + } else { + // otherwise, increase the request count + u = r + n; + // and check for long overflow + if (u < 0) { + // cap at max value, which is essentially unlimited + u = Long.MAX_VALUE; + } + } + // try setting the new request value + if (compareAndSet(r, u)) { + // if successful, notify the parent dispacher this child can receive more + // elements + parent.dispatch(); + return; + } + // otherwise, someone else changed the state (perhaps a concurrent + // request or unsubscription so retry } - drainQueue(originSubscriber); } - - private void requestMoreAfterEmission(int emitted) { - if (emitted > 0) { - OriginSubscriber origin = state.getOrigin(); - if (origin != null) { - long r = origin.originOutstanding.addAndGet(-emitted); - if (r <= origin.THRESHOLD) { - origin.requestMore(RxRingBuffer.SIZE - origin.THRESHOLD); - } + + /** + * Indicate that values have been emitted to this child subscriber by the dispatch() method. + * @param n the number of items emitted + * @return the updated request value (may indicate how much can be produced or a terminal state) + */ + public long produced(long n) { + // we don't allow producing zero or less: it would be a bug in the operator + if (n <= 0) { + throw new IllegalArgumentException("Cant produce zero or less"); + } + for (;;) { + // get the current request value + long r = get(); + // if no request has been made yet, we shouldn't have emitted to this child + // subscriber so there is a bug in this operator + if (r == NOT_REQUESTED) { + throw new IllegalStateException("Produced without request"); } + // if the child has unsubscribed, simply return and indicate this + if (r == UNSUBSCRIBED) { + return UNSUBSCRIBED; + } + // reduce the requested amount + long u = r - n; + // if the new amount is less than zero, we have a bug in this operator + if (u < 0) { + throw new IllegalStateException("More produced (" + n + ") than requested (" + r + ")"); + } + // try updating the request value + if (compareAndSet(r, u)) { + // and return the udpated value + return u; + } + // otherwise, some concurrent activity happened and we need to retry } } - - public void drainQueue(OriginSubscriber originSubscriber) { - if (WIP.getAndIncrement(this) == 0) { - State localState = state; - Map, AtomicLong> localMap = localState.ss; - RxRingBuffer localBuffer = originSubscriber.buffer; - NotificationLite nl = notifier; - - int emitted = 0; - do { - /* - * Set to 1 otherwise it could have grown very large while in the last poll loop - * and then we can end up looping all those times again here before exiting even once we've drained - */ - WIP.set(this, 1); - /** - * This is done in the most inefficient possible way right now and we can revisit the approach. - * If we want to batch this then we need to account for new subscribers arriving with a lower request count - * concurrently while iterating the batch ... or accept that they won't - */ - while (true) { - if (localState.hasNoSubscriber()) { - // Drop items due to no subscriber - if (localBuffer.poll() == null) { - // Exit due to no more item - break; - } else { - // Keep dropping cached items. - continue; - } - } - - boolean shouldEmit = localState.canEmitWithDecrement(); - if (!shouldEmit) { - break; - } - Object o = localBuffer.poll(); - if (o == null) { - // nothing in buffer so increment outstanding back again - localState.incrementOutstandingAfterFailedEmit(); - break; - } - - for (Subscriber s : localState.getSubscribers()) { - AtomicLong req = localMap.get(s); - if (req != null) { // null req indicates a concurrent unsubscription happened - nl.accept(s, o); - req.decrementAndGet(); - } - } - emitted++; - } - } while (WIP.decrementAndGet(this) > 0); - requestMoreAfterEmission(emitted); + + @Override + public boolean isUnsubscribed() { + return get() == UNSUBSCRIBED; + } + @Override + public void unsubscribe() { + long r = get(); + // let's see if we are unsubscribed + if (r != UNSUBSCRIBED) { + // if not, swap in the terminal state, this is idempotent + // because other methods using CAS won't overwrite this value, + // concurrent calls to unsubscribe will atomically swap in the same + // terminal value + r = getAndSet(UNSUBSCRIBED); + // and only one of them will see a non-terminated value before the swap + if (r != UNSUBSCRIBED) { + // remove this from the parent + parent.remove(this); + // After removal, we might have unblocked the other child subscribers: + // let's assume this child had 0 requested before the unsubscription while + // the others had non-zero. By removing this 'blocking' child, the others + // are now free to receive events + parent.dispatch(); + } } } } diff --git a/src/perf/java/rx/operators/OperatorPublishPerf.java b/src/perf/java/rx/operators/OperatorPublishPerf.java index 0e9a1d65c5..1658917c29 100644 --- a/src/perf/java/rx/operators/OperatorPublishPerf.java +++ b/src/perf/java/rx/operators/OperatorPublishPerf.java @@ -24,7 +24,6 @@ import rx.*; import rx.Observable; -import rx.functions.Action0; import rx.observables.ConnectableObservable; import rx.schedulers.Schedulers; @@ -90,7 +89,6 @@ public void onCompleted() { private ConnectableObservable source; private Observable observable; - private CyclicBarrier sourceDone; @Setup public void setup() { List list = new ArrayList(); @@ -98,25 +96,6 @@ public void setup() { list.add(i); } Observable src = Observable.from(list); - if (childCount == 0) { - sourceDone = new CyclicBarrier(2); - src = src - // for childCount == 0, make sure we measure how fast the source is depleted - .doOnCompleted(new Action0() { - @Override - public void call() { - try { - sourceDone.await(2, TimeUnit.SECONDS); - } catch (InterruptedException ex) { - // ignored - } catch (BrokenBarrierException ex) { - // ignored - } catch (TimeoutException ex) { - // ignored - } - } - }); - } source = src.publish(); observable = async ? source.observeOn(Schedulers.computation()) : source; } @@ -137,23 +116,20 @@ public void benchmark(Blackhole bh) throws InterruptedException, Subscription s = source.connect(); - if (cc == 0) { - sourceDone.await(2, TimeUnit.SECONDS); - } if (completion != null && !completion.await(2, TimeUnit.SECONDS)) { throw new RuntimeException("Source hung!"); } s.unsubscribe(); } -// public static void main(String[] args) throws Exception { -// OperatorPublishPerf o = new OperatorPublishPerf(); -// o.async = true; -// o.batchFrequency = 1; -// o.childCount = 1; -// o.size = 1000000; -// o.setup(); -// for (int j = 0; j < 1000; j++) { -// o.benchmark(null); -// } -// } + public static void main(String[] args) throws Exception { + OperatorPublishPerf o = new OperatorPublishPerf(); + o.async = false; + o.batchFrequency = 1; + o.childCount = 0; + o.size = 1; + o.setup(); + for (int j = 0; j < 1000; j++) { + o.benchmark(null); + } + } } diff --git a/src/test/java/rx/internal/operators/OperatorPublishTest.java b/src/test/java/rx/internal/operators/OperatorPublishTest.java index 3d8481a676..f6bfaa7e21 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishTest.java @@ -17,7 +17,7 @@ import static org.junit.Assert.*; -import java.util.Arrays; +import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; @@ -25,12 +25,12 @@ import rx.*; import rx.Observable.OnSubscribe; +import rx.Observable; import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observables.ConnectableObservable; import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.schedulers.TestScheduler; +import rx.schedulers.*; public class OperatorPublishTest { @@ -259,4 +259,160 @@ public void testConnectWithNoSubscriber() { subscriber.assertNoErrors(); subscriber.assertTerminalEvent(); } + + @Test + public void testSubscribeAfterDisconnectThenConnect() { + ConnectableObservable source = Observable.just(1).publish(); + + TestSubscriber ts1 = new TestSubscriber(); + + source.subscribe(ts1); + + Subscription s = source.connect(); + + ts1.assertReceivedOnNext(Arrays.asList(1)); + ts1.assertNoErrors(); + ts1.assertTerminalEvent(); + + TestSubscriber ts2 = new TestSubscriber(); + + source.subscribe(ts2); + + Subscription s2 = source.connect(); + + ts2.assertReceivedOnNext(Arrays.asList(1)); + ts2.assertNoErrors(); + ts2.assertTerminalEvent(); + + System.out.println(s); + System.out.println(s2); + } + + @Test + public void testNoSubscriberRetentionOnCompleted() { + OperatorPublish source = (OperatorPublish)Observable.just(1).publish(); + + TestSubscriber ts1 = new TestSubscriber(); + + source.unsafeSubscribe(ts1); + + ts1.assertReceivedOnNext(Arrays.asList()); + ts1.assertNoErrors(); + assertTrue(ts1.getOnCompletedEvents().isEmpty()); + + source.connect(); + + ts1.assertReceivedOnNext(Arrays.asList(1)); + ts1.assertNoErrors(); + ts1.assertTerminalEvent(); + + assertNull(source.current.get()); + } + + @Test + public void testNonNullConnection() { + ConnectableObservable source = Observable.never().publish(); + + assertNotNull(source.connect()); + assertNotNull(source.connect()); + } + + @Test + public void testNoDisconnectSomeoneElse() { + ConnectableObservable source = Observable.never().publish(); + + Subscription s1 = source.connect(); + Subscription s2 = source.connect(); + + s1.unsubscribe(); + + Subscription s3 = source.connect(); + + s2.unsubscribe(); + + assertTrue(s1.isUnsubscribed()); + assertTrue(s2.isUnsubscribed()); + assertFalse(s3.isUnsubscribed()); + } + + @Test + public void testZeroRequested() { + ConnectableObservable source = Observable.just(1).publish(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(0); + } + }; + + source.subscribe(ts); + + ts.assertReceivedOnNext(Arrays.asList()); + ts.assertNoErrors(); + assertTrue(ts.getOnCompletedEvents().isEmpty()); + + source.connect(); + + ts.assertReceivedOnNext(Arrays.asList()); + ts.assertNoErrors(); + assertTrue(ts.getOnCompletedEvents().isEmpty()); + + ts.requestMore(5); + + ts.assertReceivedOnNext(Arrays.asList(1)); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + } + @Test + public void testConnectIsIdempotent() { + final AtomicInteger calls = new AtomicInteger(); + Observable source = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + calls.getAndIncrement(); + } + }); + + ConnectableObservable conn = source.publish(); + + assertEquals(0, calls.get()); + + conn.connect(); + conn.connect(); + + assertEquals(1, calls.get()); + + conn.connect().unsubscribe(); + + conn.connect(); + conn.connect(); + + assertEquals(2, calls.get()); + } + @Test + public void testObserveOn() { + ConnectableObservable co = Observable.range(0, 1000).publish(); + Observable obs = co.observeOn(Schedulers.computation()); + for (int i = 0; i < 1000; i++) { + for (int j = 1; j < 6; j++) { + List> tss = new ArrayList>(); + for (int k = 1; k < j; k++) { + TestSubscriber ts = new TestSubscriber(); + tss.add(ts); + obs.subscribe(ts); + } + + Subscription s = co.connect(); + + for (TestSubscriber ts : tss) { + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts.assertTerminalEvent(); + ts.assertNoErrors(); + assertEquals(1000, ts.getOnNextEvents().size()); + } + s.unsubscribe(); + } + } + } } From 729e90e925cf43c745e83456ac06456f36018e3d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 23 Apr 2015 17:48:54 +0200 Subject: [PATCH 244/857] Fix the performance degradation due to different schedule execution and SubscriptionList.add() and thread unparking. --- .../schedulers/EventLoopsScheduler.java | 5 +-- .../rx/internal/util/SubscriptionList.java | 33 ++++--------------- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java index 71c4397754..986ea6d467 100644 --- a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java @@ -117,10 +117,7 @@ public Subscription schedule(Action0 action) { if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } - ScheduledAction s = poolWorker.scheduleActual(action, 0, null); - - serial.add(s); - s.addParent(serial); + ScheduledAction s = poolWorker.scheduleActual(action, 0, null, serial); return s; } diff --git a/src/main/java/rx/internal/util/SubscriptionList.java b/src/main/java/rx/internal/util/SubscriptionList.java index a3a91fa1b0..6f6f391dde 100644 --- a/src/main/java/rx/internal/util/SubscriptionList.java +++ b/src/main/java/rx/internal/util/SubscriptionList.java @@ -15,12 +15,7 @@ */ package rx.internal.util; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; +import java.util.*; import rx.Subscription; import rx.exceptions.Exceptions; @@ -34,7 +29,6 @@ public final class SubscriptionList implements Subscription { private LinkedList subscriptions; private volatile boolean unsubscribed; - private final ReentrantLock lock = new ReentrantLock(); public SubscriptionList() { } @@ -66,8 +60,7 @@ public void add(final Subscription s) { return; } if (!unsubscribed) { - lock.lock(); - try { + synchronized (this) { if (!unsubscribed) { LinkedList subs = subscriptions; if (subs == null) { @@ -77,8 +70,6 @@ public void add(final Subscription s) { subs.add(s); return; } - } finally { - lock.unlock(); } } // call after leaving the synchronized block so we're not holding a lock while executing this @@ -88,15 +79,12 @@ public void add(final Subscription s) { public void remove(final Subscription s) { if (!unsubscribed) { boolean unsubscribe = false; - lock.lock(); - try { + synchronized (this) { LinkedList subs = subscriptions; if (unsubscribed || subs == null) { return; } unsubscribe = subs.remove(s); - } finally { - lock.unlock(); } if (unsubscribe) { // if we removed successfully we then need to call unsubscribe on it (outside of the lock) @@ -113,16 +101,13 @@ public void remove(final Subscription s) { public void unsubscribe() { if (!unsubscribed) { List list; - lock.lock(); - try { + synchronized (this) { if (unsubscribed) { return; } unsubscribed = true; list = subscriptions; subscriptions = null; - } finally { - lock.unlock(); } // we will only get here once unsubscribeFromAll(list); @@ -150,12 +135,9 @@ private static void unsubscribeFromAll(Collection subscriptions) { public void clear() { if (!unsubscribed) { List list; - lock.lock(); - try { + synchronized (this) { list = subscriptions; subscriptions = null; - } finally { - lock.unlock(); } unsubscribeFromAll(list); } @@ -166,11 +148,8 @@ public void clear() { */ public boolean hasSubscriptions() { if (!unsubscribed) { - lock.lock(); - try { + synchronized (this) { return !unsubscribed && subscriptions != null && !subscriptions.isEmpty(); - } finally { - lock.unlock(); } } return false; From 9d96b793d0ec54912365d79bf7e172422e76a556 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 16 Apr 2015 21:01:08 +1000 Subject: [PATCH 245/857] add OperatorTakeLastOne --- src/main/java/rx/Observable.java | 7 +- .../operators/OperatorTakeLastOne.java | 173 ++++++++++++++++++ .../rx/operators/OperatorTakeLastOnePerf.java | 39 ++++ .../operators/OperatorTakeLastOneTest.java | 128 +++++++++++++ 4 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 src/main/java/rx/internal/operators/OperatorTakeLastOne.java create mode 100644 src/perf/java/rx/operators/OperatorTakeLastOnePerf.java create mode 100644 src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 38249d44e6..ead34cba61 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7771,7 +7771,12 @@ public final Observable takeFirst(Func1 predicate) { * @see ReactiveX operators documentation: TakeLast */ public final Observable takeLast(final int count) { - return lift(new OperatorTakeLast(count)); + if (count == 0) + return ignoreElements(); + else if (count == 1 ) + return lift(OperatorTakeLastOne.instance()); + else + return lift(new OperatorTakeLast(count)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorTakeLastOne.java b/src/main/java/rx/internal/operators/OperatorTakeLastOne.java new file mode 100644 index 0000000000..a9bb7b5d33 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorTakeLastOne.java @@ -0,0 +1,173 @@ +package rx.internal.operators; + +import java.util.concurrent.atomic.AtomicInteger; + +import rx.Observable.Operator; +import rx.Producer; +import rx.Subscriber; + +public class OperatorTakeLastOne implements Operator { + + private static class Holder { + static final OperatorTakeLastOne INSTANCE = new OperatorTakeLastOne(); + } + + @SuppressWarnings("unchecked") + public static OperatorTakeLastOne instance() { + return (OperatorTakeLastOne) Holder.INSTANCE; + } + + private OperatorTakeLastOne() { + + } + + @Override + public Subscriber call(Subscriber child) { + final ParentSubscriber parent = new ParentSubscriber(child); + child.setProducer(new Producer() { + + @Override + public void request(long n) { + parent.requestMore(n); + } + }); + child.add(parent); + return parent; + } + + private static class ParentSubscriber extends Subscriber { + + private final static int NOT_REQUESTED_NOT_COMPLETED = 0; + private final static int NOT_REQUESTED_COMPLETED = 1; + private final static int REQUESTED_NOT_COMPLETED = 2; + private final static int REQUESTED_COMPLETED = 3; + + /* + * These are the expected state transitions: + * + * NOT_REQUESTED_NOT_COMPLETED --> REQUESTED_NOT_COMPLETED + * | | + * V V + * NOT_REQUESTED_COMPLETED --> REQUESTED_COMPLETED + * + * Once at REQUESTED_COMPLETED we emit the last value if one exists + */ + + // Used as the initial value of last + private static final Object ABSENT = new Object(); + + // the downstream subscriber + private final Subscriber child; + + @SuppressWarnings("unchecked") + // we can get away with this cast at runtime because of type erasure + private T last = (T) ABSENT; + + // holds the current state of the stream so that we can make atomic + // updates to it + private final AtomicInteger state = new AtomicInteger(NOT_REQUESTED_NOT_COMPLETED); + + ParentSubscriber(Subscriber child) { + this.child = child; + } + + void requestMore(long n) { + if (n > 0) { + // CAS loop to atomically change state given that onCompleted() + // or another requestMore() may be acting concurrently + while (true) { + // read the value of state and then try state transitions + // only if the value of state does not change in the + // meantime (in another requestMore() or onCompleted()). If + // the value has changed and we expect to do a transition + // still then we loop and try again. + final int s = state.get(); + if (s == NOT_REQUESTED_NOT_COMPLETED) { + if (state.compareAndSet(NOT_REQUESTED_NOT_COMPLETED, + REQUESTED_NOT_COMPLETED)) { + return; + } + } else if (s == NOT_REQUESTED_COMPLETED) { + if (state.compareAndSet(NOT_REQUESTED_COMPLETED, REQUESTED_COMPLETED)) { + emit(); + return; + } + } else + // already requested so we exit + return; + } + } + } + + @Override + public void onCompleted() { + //shortcut if an empty stream + if (last == ABSENT) { + child.onCompleted(); + return; + } + // CAS loop to atomically change state given that requestMore() + // may be acting concurrently + while (true) { + // read the value of state and then try state transitions + // only if the value of state does not change in the meantime + // (in another requestMore()). If the value has changed and + // we expect to do a transition still then we loop and try + // again. + final int s = state.get(); + if (s == NOT_REQUESTED_NOT_COMPLETED) { + if (state.compareAndSet(NOT_REQUESTED_NOT_COMPLETED, NOT_REQUESTED_COMPLETED)) { + return; + } + } else if (s == REQUESTED_NOT_COMPLETED) { + if (state.compareAndSet(REQUESTED_NOT_COMPLETED, REQUESTED_COMPLETED)) { + emit(); + return; + } + } else + // already completed so we exit + return; + } + } + + /** + * If not unsubscribed then emits last value and completed to the child + * subscriber. + */ + private void emit() { + if (isUnsubscribed()) { + // release for gc + last = null; + return; + } + // Note that last is safely published despite not being volatile + // because a CAS update must have happened in the current thread just before + // emit() was called + T t = last; + // release for gc + last = null; + if (t != ABSENT) { + try { + child.onNext(t); + } catch (Throwable e) { + child.onError(e); + return; + } + } + if (!isUnsubscribed()) + child.onCompleted(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + last = t; + } + + } + +} diff --git a/src/perf/java/rx/operators/OperatorTakeLastOnePerf.java b/src/perf/java/rx/operators/OperatorTakeLastOnePerf.java new file mode 100644 index 0000000000..43adf76bc5 --- /dev/null +++ b/src/perf/java/rx/operators/OperatorTakeLastOnePerf.java @@ -0,0 +1,39 @@ +package rx.operators; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +import rx.internal.operators.OperatorTakeLast; +import rx.internal.operators.OperatorTakeLastOne; +import rx.jmh.InputWithIncrementingInteger; + +public class OperatorTakeLastOnePerf { + + private static final OperatorTakeLast TAKE_LAST = new OperatorTakeLast(1); + + @State(Scope.Thread) + public static class Input extends InputWithIncrementingInteger { + + @Param({ "5", "100", "1000000" }) + public int size; + + @Override + public int getSize() { + return size; + } + + } + + @Benchmark + public void takeLastOneUsingTakeLast(Input input) { + input.observable.lift(TAKE_LAST).subscribe(input.observer); + } + + @Benchmark + public void takeLastOneUsingTakeLastOne(Input input) { + input.observable.lift(OperatorTakeLastOne.instance()).subscribe(input.observer); + } + +} diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java new file mode 100644 index 0000000000..3ca921daf0 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java @@ -0,0 +1,128 @@ +package rx.internal.operators; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import rx.Observable; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.observers.TestSubscriber; + +public class OperatorTakeLastOneTest { + + @Test + public void testLastOfManyReturnsLast() { + TestSubscriber s = new TestSubscriber(); + Observable.range(1, 10).takeLast(1).subscribe(s); + s.assertReceivedOnNext(Arrays.asList(10)); + s.assertNoErrors(); + s.assertTerminalEvent(); + s.assertUnsubscribed(); + } + + @Test + public void testLastOfEmptyReturnsEmpty() { + TestSubscriber s = new TestSubscriber(); + Observable.empty().takeLast(1).subscribe(s); + s.assertReceivedOnNext(Collections.emptyList()); + s.assertNoErrors(); + s.assertTerminalEvent(); + s.assertUnsubscribed(); + } + + @Test + public void testLastOfOneReturnsLast() { + TestSubscriber s = new TestSubscriber(); + Observable.just(1).takeLast(1).subscribe(s); + s.assertReceivedOnNext(Arrays.asList(1)); + s.assertNoErrors(); + s.assertTerminalEvent(); + s.assertUnsubscribed(); + } + + @Test + public void testUnsubscribesFromUpstream() { + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Action0 unsubscribeAction = new Action0() { + @Override + public void call() { + unsubscribed.set(true); + } + }; + Observable.just(1).doOnUnsubscribe(unsubscribeAction) + .takeLast(1).subscribe(); + assertTrue(unsubscribed.get()); + } + + @Test + public void testLastWithBackpressure() { + MySubscriber s = new MySubscriber(0); + Observable.just(1).takeLast(1).subscribe(s); + assertEquals(0, s.list.size()); + s.requestMore(1); + assertEquals(1, s.list.size()); + } + + @Test + public void testTakeLastZeroProcessesAllItemsButIgnoresThem() { + final AtomicInteger upstreamCount = new AtomicInteger(); + final int num = 10; + int count = Observable.range(1,num).doOnNext(new Action1() { + + @Override + public void call(Integer t) { + upstreamCount.incrementAndGet(); + }}) + .takeLast(0).count().toBlocking().single(); + assertEquals(num, upstreamCount.get()); + assertEquals(0, count); + } + + private static class MySubscriber extends Subscriber { + + private long initialRequest; + + MySubscriber(long initialRequest) { + this.initialRequest = initialRequest; + } + + final List list = new ArrayList(); + + public void requestMore(long n) { + request(n); + } + + @Override + public void onStart() { + request(initialRequest); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(T t) { + list.add(t); + } + + } + +} From 6db98f8750f580995657f83b5620b579c97e6a06 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 25 Apr 2015 18:27:30 +1000 Subject: [PATCH 246/857] improve perf of OperatorIgnoreElements --- src/main/java/rx/Observable.java | 2 +- .../operators/OperatorIgnoreElements.java | 60 ++++++++ .../operators/OperatorIgnoreElementsTest.java | 130 ++++++++++++++++++ .../operators/OperatorTakeLastOneTest.java | 1 - 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OperatorIgnoreElements.java create mode 100644 src/test/java/rx/internal/operators/OperatorIgnoreElementsTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index ead34cba61..b74b27c683 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4941,7 +4941,7 @@ public final Observable groupJoin(Observable right, Func1 * @see ReactiveX operators documentation: IgnoreElements */ public final Observable ignoreElements() { - return filter(UtilityFunctions.alwaysFalse()); + return lift(OperatorIgnoreElements. instance()); } /** diff --git a/src/main/java/rx/internal/operators/OperatorIgnoreElements.java b/src/main/java/rx/internal/operators/OperatorIgnoreElements.java new file mode 100644 index 0000000000..3f38d8e585 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorIgnoreElements.java @@ -0,0 +1,60 @@ +/** + * 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.Operator; +import rx.Subscriber; + +public class OperatorIgnoreElements implements Operator { + + private static class Holder { + static final OperatorIgnoreElements INSTANCE = new OperatorIgnoreElements(); + } + + @SuppressWarnings("unchecked") + public static OperatorIgnoreElements instance() { + return (OperatorIgnoreElements) Holder.INSTANCE; + } + + private OperatorIgnoreElements() { + + } + + @Override + public Subscriber call(final Subscriber child) { + Subscriber parent = new Subscriber() { + + @Override + public void onCompleted() { + child.onCompleted(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + // ignore element + } + + }; + child.add(parent); + return parent; + } + +} diff --git a/src/test/java/rx/internal/operators/OperatorIgnoreElementsTest.java b/src/test/java/rx/internal/operators/OperatorIgnoreElementsTest.java new file mode 100644 index 0000000000..818f228ba8 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorIgnoreElementsTest.java @@ -0,0 +1,130 @@ +package rx.internal.operators; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.observers.TestSubscriber; + +public class OperatorIgnoreElementsTest { + + @Test + public void testWithEmpty() { + assertTrue(Observable.empty().ignoreElements().isEmpty().toBlocking().single()); + } + + @Test + public void testWithNonEmpty() { + assertTrue(Observable.just(1, 2, 3).ignoreElements().isEmpty().toBlocking().single()); + } + + @Test + public void testUpstreamIsProcessedButIgnored() { + final int num = 10; + final AtomicInteger upstreamCount = new AtomicInteger(); + int count = Observable.range(1, num) + .doOnNext(new Action1() { + @Override + public void call(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + .ignoreElements() + .count().toBlocking().single(); + assertEquals(num, upstreamCount.get()); + assertEquals(0, count); + } + + @Test + public void testCompletedOk() { + TestSubscriber ts = new TestSubscriber(); + Observable.range(1, 10).ignoreElements().subscribe(ts); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList()); + ts.assertTerminalEvent(); + ts.assertUnsubscribed(); + } + + @Test + public void testErrorReceived() { + TestSubscriber ts = new TestSubscriber(); + RuntimeException ex = new RuntimeException("boo"); + Observable.error(ex).ignoreElements().subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList()); + ts.assertTerminalEvent(); + ts.assertUnsubscribed(); + assertEquals(1, ts.getOnErrorEvents().size()); + assertEquals("boo", ts.getOnErrorEvents().get(0).getMessage()); + } + + @Test + public void testUnsubscribesFromUpstream() { + final AtomicBoolean unsub = new AtomicBoolean(); + Observable.range(1, 10).doOnUnsubscribe(new Action0() { + @Override + public void call() { + unsub.set(true); + }}) + .subscribe(); + assertTrue(unsub.get()); + } + + @Test(timeout = 10000) + public void testDoesNotHangAndProcessesAllUsingBackpressure() { + final AtomicInteger upstreamCount = new AtomicInteger(); + final AtomicInteger count = new AtomicInteger(0); + int num = 10; + Observable.range(1, num) + // + .doOnNext(new Action1() { + @Override + public void call(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + // + .ignoreElements() + // + .doOnNext(new Action1() { + + @Override + public void call(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + // + .subscribe(new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + } + }); + assertEquals(num, upstreamCount.get()); + assertEquals(0, count.get()); + } + +} diff --git a/src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java b/src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java index 3ca921daf0..2dcae73fc0 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeLastOneTest.java @@ -14,7 +14,6 @@ import rx.Observable; import rx.Subscriber; -import rx.Subscription; import rx.functions.Action0; import rx.functions.Action1; import rx.observers.TestSubscriber; From f6ca9ee01c835a5be384ca5dfecf97dc740e2ad6 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 29 Apr 2015 13:33:58 +1000 Subject: [PATCH 247/857] prevent request overflow in OperatorObserveOn and add unit test that fails on original codebase but passes with change --- .../internal/operators/OperatorObserveOn.java | 19 ++++++--- .../operators/OperatorObserveOnTest.java | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index af08f2a1b9..ab0f5ca501 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -16,15 +16,22 @@ package rx.internal.operators; import java.util.Queue; -import java.util.concurrent.atomic.*; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; import rx.Observable.Operator; -import rx.*; +import rx.Producer; +import rx.Scheduler; +import rx.Subscriber; +import rx.Subscription; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; -import rx.internal.util.*; -import rx.internal.util.unsafe.*; -import rx.schedulers.*; +import rx.internal.util.RxRingBuffer; +import rx.internal.util.SynchronizedQueue; +import rx.internal.util.unsafe.SpscArrayQueue; +import rx.internal.util.unsafe.UnsafeAccess; +import rx.schedulers.ImmediateScheduler; +import rx.schedulers.TrampolineScheduler; /** * Delivers events on the specified {@code Scheduler} asynchronously via an unbounded buffer. @@ -96,7 +103,7 @@ public ObserveOnSubscriber(Scheduler scheduler, Subscriber child) { @Override public void request(long n) { - REQUESTED.getAndAdd(ObserveOnSubscriber.this, n); + BackpressureUtils.getAndAddRequest(REQUESTED, ObserveOnSubscriber.this, n); schedule(); } diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index de204a82ec..b0c8a5bcfd 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -724,4 +724,45 @@ public Long call(Long t1, Integer t2) { assertEquals(MissingBackpressureException.class, ts.getOnErrorEvents().get(0).getClass()); } + @Test + public void testRequestOverflow() throws InterruptedException { + + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger count = new AtomicInteger(); + Observable.range(1, 100).observeOn(Schedulers.computation()) + .subscribe(new Subscriber() { + + boolean first = true; + + @Override + public void onStart() { + request(2); + } + + @Override + public void onCompleted() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + if (first) { + request(Long.MAX_VALUE - 1); + request(Long.MAX_VALUE - 1); + request(10); + first = false; + } + } + }); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + assertEquals(100, count.get()); + + } + } From fcfa4e83845ca21a5a622a726d346b46c487dce0 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 29 Apr 2015 17:34:54 +1000 Subject: [PATCH 248/857] ensure this does not escape from ObserveOnSubscriber constructor by moving code to an init() method and calling after construction --- .../java/rx/internal/operators/OperatorObserveOn.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index ab0f5ca501..13a78ca14c 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -61,7 +61,9 @@ public Subscriber call(Subscriber child) { // avoid overhead, execute directly return child; } else { - return new ObserveOnSubscriber(scheduler, child); + ObserveOnSubscriber parent = new ObserveOnSubscriber(scheduler, child); + parent.init(); + return parent; } } @@ -98,6 +100,11 @@ public ObserveOnSubscriber(Scheduler scheduler, Subscriber child) { queue = new SynchronizedQueue(RxRingBuffer.SIZE); } this.scheduledUnsubscribe = new ScheduledUnsubscribe(recursiveScheduler); + } + + void init() { + // don't want this code in the constructor because `this` can escape through the + // setProducer call child.add(scheduledUnsubscribe); child.setProducer(new Producer() { From 0ddd75f05e39a680bc99217c7e1cd5f9b11d167c Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 29 Apr 2015 16:25:38 +0200 Subject: [PATCH 249/857] Non-blocking version of the toBlocking().latest() operator. --- src/main/java/rx/Observable.java | 21 ++ .../OperatorOnBackpressureLatest.java | 225 ++++++++++++++++++ .../OperatorOnBackpressureLatestTest.java | 147 ++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java create mode 100644 src/test/java/rx/internal/operators/OperatorOnBackpressureLatestTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index cefe16e44a..e8f595d955 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5367,6 +5367,27 @@ public final Observable onBackpressureBlock() { return onBackpressureBlock(rx.internal.util.RxRingBuffer.SIZE); } + /** + * Instructs an Observable that is emitting items faster than its observer can consume them to + * hold onto the latest value and emit that on request. + *

+ * Its behavior is logically equivalent to toBlocking().latest() with the exception that + * the downstream is not blocking while requesting more values. + *

+ * Note that if the upstream Observable does support backpressure, this operator ignores that capability + * and doesn't propagate any backpressure requests from downstream. + *

+ * Note that due to the nature of how backpressure requests are propagated through subscribeOn/observeOn, + * requesting more than 1 from downstream doesn't guarantee a continuous delivery of onNext events. + * @return + * @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 onBackpressureLatest() { + return lift(OperatorOnBackpressureLatest.instance()); + } + /** * Instructs an Observable to pass control to another Observable rather than invoking * {@link Observer#onError onError} if it encounters an error. diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java new file mode 100644 index 0000000000..512010515c --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java @@ -0,0 +1,225 @@ +/** + * 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.atomic.*; + +import rx.Observable.Operator; +import rx.*; + +/** + * An operator which drops all but the last received value in case the downstream + * doesn't request more. + */ +public final class OperatorOnBackpressureLatest implements Operator { + /** Holds a singleton instance initialized on class-loading. */ + static final class Holder { + static final OperatorOnBackpressureLatest INSTANCE = new OperatorOnBackpressureLatest(); + } + + /** + * Returns a singleton instance of the OnBackpressureLatest operator since it is stateless. + * @return the single instanceof OperatorOnBackpressureLatest + */ + @SuppressWarnings("unchecked") + public static OperatorOnBackpressureLatest instance() { + return (OperatorOnBackpressureLatest)Holder.INSTANCE; + } + + @Override + public Subscriber call(Subscriber child) { + final LatestEmitter producer = new LatestEmitter(child); + LatestSubscriber parent = new LatestSubscriber(producer); + producer.parent = parent; + child.add(parent); + child.add(producer); + child.setProducer(producer); + return parent; + } + /** + * A terminatable producer which emits the latest items on request. + * @param + */ + static final class LatestEmitter extends AtomicLong implements Producer, Subscription, Observer { + /** */ + private static final long serialVersionUID = -1364393685005146274L; + final Subscriber child; + LatestSubscriber parent; + final AtomicReference value; + /** Written before done, read after done. */ + Throwable terminal; + volatile boolean done; + /** Guarded by this. */ + boolean emitting; + /** Guarded by this. */ + boolean missed; + static final Object EMPTY = new Object(); + static final long NOT_REQUESTED = Long.MIN_VALUE / 2; + public LatestEmitter(Subscriber child) { + this.child = child; + this.value = new AtomicReference(EMPTY); + this.lazySet(NOT_REQUESTED); // not + } + @Override + public void request(long n) { + if (n >= 0) { + for (;;) { + long r = get(); + if (r == Long.MIN_VALUE) { + return; + } + long u; + if (r == NOT_REQUESTED) { + u = n; + } else { + u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + } + if (compareAndSet(r, u)) { + if (r == NOT_REQUESTED) { + parent.requestMore(Long.MAX_VALUE); + } + emit(); + return; + } + } + } + } + long produced(long n) { + for (;;) { + long r = get(); + if (r < 0) { + return r; + } + long u = r - n; + if (compareAndSet(r, u)) { + return u; + } + } + } + @Override + public boolean isUnsubscribed() { + return get() == Long.MIN_VALUE; + } + @Override + public void unsubscribe() { + if (get() >= 0) { + getAndSet(Long.MIN_VALUE); + } + } + + @Override + public void onNext(T t) { + value.lazySet(t); // emit's synchronized block does a full release + emit(); + } + @Override + public void onError(Throwable e) { + terminal = e; + done = true; + emit(); + } + @Override + public void onCompleted() { + done = true; + emit(); + } + void emit() { + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + missed = false; + } + boolean skipFinal = false; + try { + for (;;) { + long r = get(); + if (r == Long.MIN_VALUE) { + skipFinal = true; + break; + } + Object v = value.get(); + if (r > 0 && v != EMPTY) { + @SuppressWarnings("unchecked") + T v2 = (T)v; + child.onNext(v2); + value.compareAndSet(v, EMPTY); + produced(1); + v = EMPTY; + } + if (v == EMPTY && done) { + Throwable e = terminal; + if (e != null) { + child.onError(e); + } else { + child.onCompleted(); + } + } + synchronized (this) { + if (!missed) { + emitting = false; + skipFinal = true; + break; + } + missed = false; + } + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + } + static final class LatestSubscriber extends Subscriber { + private final LatestEmitter producer; + + private LatestSubscriber(LatestEmitter producer) { + this.producer = producer; + } + + @Override + public void onStart() { + // don't run until the child actually requested to avoid synchronous problems + request(0); + } + + @Override + public void onNext(T t) { + producer.onNext(t); + } + + @Override + public void onError(Throwable e) { + producer.onError(e); + } + + @Override + public void onCompleted() { + producer.onCompleted(); + } + void requestMore(long n) { + request(n); + } + } +} diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureLatestTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureLatestTest.java new file mode 100644 index 0000000000..2b40ac772b --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureLatestTest.java @@ -0,0 +1,147 @@ +/** + * 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.*; +import java.util.concurrent.TimeUnit; + +import org.junit.*; + +import rx.Observable; +import rx.exceptions.TestException; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; + +public class OperatorOnBackpressureLatestTest { + @Test + public void testSimple() { + TestSubscriber ts = new TestSubscriber(); + + Observable.range(1, 5).onBackpressureLatest().subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); + } + @Test + public void testSimpleError() { + TestSubscriber ts = new TestSubscriber(); + + Observable.range(1, 5).concatWith(Observable.error(new TestException())) + .onBackpressureLatest().subscribe(ts); + + ts.assertTerminalEvent(); + Assert.assertEquals(1, ts.getOnErrorEvents().size()); + Assert.assertTrue(ts.getOnErrorEvents().get(0) instanceof TestException); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); + } + @Test + public void testSimpleBackpressure() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(2); + } + }; + + Observable.range(1, 5).onBackpressureLatest().subscribe(ts); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2)); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + } + @Test + public void testSynchronousDrop() { + PublishSubject source = PublishSubject.create(); + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(0); + } + }; + + source.onBackpressureLatest().subscribe(ts); + + ts.assertReceivedOnNext(Collections.emptyList()); + + source.onNext(1); + ts.requestMore(2); + + ts.assertReceivedOnNext(Arrays.asList(1)); + + source.onNext(2); + + ts.assertReceivedOnNext(Arrays.asList(1, 2)); + + source.onNext(3); + source.onNext(4); + source.onNext(5); + source.onNext(6); + + ts.requestMore(2); + + ts.assertReceivedOnNext(Arrays.asList(1, 2, 6)); + + source.onNext(7); + + ts.assertReceivedOnNext(Arrays.asList(1, 2, 6, 7)); + + source.onNext(8); + source.onNext(9); + source.onCompleted(); + + ts.requestMore(1); + + ts.assertReceivedOnNext(Arrays.asList(1, 2, 6, 7, 9)); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + } + @Test + public void testAsynchronousDrop() throws InterruptedException { + TestSubscriber ts = new TestSubscriber() { + final Random rnd = new Random(); + @Override + public void onStart() { + request(1); + } + @Override + public void onNext(Integer t) { + super.onNext(t); + if (rnd.nextDouble() < 0.001) { + try { + Thread.sleep(1); + } catch(InterruptedException ex) { + ex.printStackTrace(); + } + } + request(1); + } + }; + int m = 100000; + Observable.range(1, m) + .subscribeOn(Schedulers.computation()) + .onBackpressureLatest() + .observeOn(Schedulers.io()) + .subscribe(ts); + + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts.assertTerminalEvent(); + int n = ts.getOnNextEvents().size(); + System.out.println("testAsynchronousDrop -> " + n); + Assert.assertTrue("All events received?", n < m); + } +} From 5e41b0482d071efa9f7d86ad5b4589dc815ad72c Mon Sep 17 00:00:00 2001 From: David Gross Date: Wed, 29 Apr 2015 14:00:02 -0700 Subject: [PATCH 250/857] Javadocs: adding @since annotations to new public methods, cleaning up Subject javadocs a bit --- src/main/java/rx/Observable.java | 5 +++-- src/main/java/rx/subjects/Subject.java | 31 +++++++++++++++++++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index cefe16e44a..206b8f3390 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -2597,8 +2597,7 @@ public final static Observable using( * @return the Observable 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) + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public final static Observable using( @@ -8857,6 +8856,7 @@ public final Observable> toSortedList(Func2ReactiveX operators documentation: To + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public final Observable> toSortedList(int initialCapacity) { @@ -8883,6 +8883,7 @@ public final Observable> toSortedList(int initialCapacity) { * @return an Observable that emits a list that contains the items emitted by the source Observable in * sorted order * @see ReactiveX operators documentation: To + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public final Observable> toSortedList(Func2 sortFunction, int initialCapacity) { diff --git a/src/main/java/rx/subjects/Subject.java b/src/main/java/rx/subjects/Subject.java index 5aa8ba4b85..4e5db6b770 100644 --- a/src/main/java/rx/subjects/Subject.java +++ b/src/main/java/rx/subjects/Subject.java @@ -30,6 +30,7 @@ protected Subject(OnSubscribe onSubscribe) { /** * Indicates whether the {@link Subject} has {@link Observer Observers} subscribed to it. + * * @return true if there is at least one Observer subscribed to this Subject, false otherwise */ public abstract boolean hasObservers(); @@ -58,7 +59,9 @@ public final SerializedSubject toSerialized() { /** * Check if the Subject has terminated with an exception. *

The operation is threadsafe. - * @return true if the subject has received a throwable through {@code onError}. + * + * @return {@code true} if the subject has received a throwable through {@code onError}. + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @Experimental public boolean hasThrowable() { @@ -67,7 +70,9 @@ public boolean hasThrowable() { /** * Check if the Subject has terminated normally. *

The operation is threadsafe. - * @return true if the subject completed normally via {@code onCompleted} + * + * @return {@code true} if the subject completed normally via {@code onCompleted} + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @Experimental public boolean hasCompleted() { @@ -76,8 +81,10 @@ public boolean hasCompleted() { /** * Returns the Throwable that terminated the Subject. *

The operation is threadsafe. - * @return the Throwable that terminated the Subject or {@code null} if the - * subject hasn't terminated yet or it terminated normally. + * + * @return the Throwable that terminated the Subject or {@code null} if the subject hasn't terminated yet or + * if it terminated normally. + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @Experimental public Throwable getThrowable() { @@ -89,7 +96,9 @@ public Throwable getThrowable() { *

Note that unless {@link #hasCompleted()} or {@link #hasThrowable()} returns true, the value * retrieved by {@code getValue()} may get outdated. *

The operation is threadsafe. - * @return true if and only if the subject has some value but not an error + * + * @return {@code true} if and only if the subject has some value but not an error + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @Experimental public boolean hasValue() { @@ -102,8 +111,10 @@ public boolean hasValue() { * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an * exception or the Subject terminated without receiving any value. *

The operation is threadsafe. - * @return the current value or {@code null} if the Subject doesn't have a value, - * has terminated with an exception or has an actual {@code null} as a value. + * + * @return the current value or {@code null} if the Subject doesn't have a value, has terminated with an + * exception or has an actual {@code null} as a value. + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @Experimental public T getValue() { @@ -114,7 +125,9 @@ public T getValue() { /** * Returns a snapshot of the currently buffered non-terminal events. *

The operation is threadsafe. + * * @return a snapshot of the currently buffered non-terminal events. + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @SuppressWarnings("unchecked") @Experimental @@ -131,10 +144,12 @@ public Object[] getValues() { *

If the subject's values fit in the specified array with room to spare * (i.e., the array has more elements than the list), the element in * the array immediately following the end of the subject's values is set to - * null. + * {@code null}. *

The operation is threadsafe. + * * @param a the array to fill in * @return the array {@code a} if it had enough capacity or a new array containing the available values + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) */ @Experimental public T[] getValues(T[] a) { From 491e615d4d1a11e583b8c7bb22ea86b49794dc8b Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 30 Apr 2015 09:45:27 +0200 Subject: [PATCH 251/857] Fixed schedule race and task retention with ExecutorScheduler. --- .../java/rx/schedulers/ExecutorScheduler.java | 126 ++++++------- .../rx/schedulers/ExecutorSchedulerTest.java | 172 +++++++++++++++++- 2 files changed, 223 insertions(+), 75 deletions(-) diff --git a/src/main/java/rx/schedulers/ExecutorScheduler.java b/src/main/java/rx/schedulers/ExecutorScheduler.java index 3f8c8899a6..ce9643cf2b 100644 --- a/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/src/main/java/rx/schedulers/ExecutorScheduler.java @@ -15,21 +15,14 @@ */ package rx.schedulers; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executor; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import rx.Scheduler; -import rx.Subscription; + +import rx.*; import rx.functions.Action0; +import rx.internal.schedulers.ScheduledAction; import rx.plugins.RxJavaPlugins; -import rx.subscriptions.CompositeSubscription; -import rx.subscriptions.MultipleAssignmentSubscription; -import rx.subscriptions.Subscriptions; +import rx.subscriptions.*; /** * Scheduler that wraps an Executor instance and establishes the Scheduler contract upon it. @@ -58,12 +51,12 @@ static final class ExecutorSchedulerWorker extends Scheduler.Worker implements R // TODO: use a better performing structure for task tracking final CompositeSubscription tasks; // TODO: use MpscLinkedQueue once available - final ConcurrentLinkedQueue queue; + final ConcurrentLinkedQueue queue; final AtomicInteger wip; public ExecutorSchedulerWorker(Executor executor) { this.executor = executor; - this.queue = new ConcurrentLinkedQueue(); + this.queue = new ConcurrentLinkedQueue(); this.wip = new AtomicInteger(); this.tasks = new CompositeSubscription(); } @@ -73,11 +66,15 @@ public Subscription schedule(Action0 action) { if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } - ExecutorAction ea = new ExecutorAction(action, tasks); + ScheduledAction ea = new ScheduledAction(action, tasks); tasks.add(ea); queue.offer(ea); if (wip.getAndIncrement() == 0) { try { + // note that since we schedule the emission of potentially multiple tasks + // there is no clear way to cancel this schedule from individual tasks + // so even if executor is an ExecutorService, we can't associate the future + // returned by submit() with any particular ScheduledAction executor.execute(this); } catch (RejectedExecutionException t) { // cleanup if rejected @@ -96,7 +93,10 @@ public Subscription schedule(Action0 action) { @Override public void run() { do { - queue.poll().run(); + ScheduledAction sa = queue.poll(); + if (!sa.isUnsubscribed()) { + sa.run(); + } } while (wip.decrementAndGet() > 0); } @@ -115,28 +115,54 @@ public Subscription schedule(final Action0 action, long delayTime, TimeUnit unit service = GenericScheduledExecutorService.getInstance(); } + final MultipleAssignmentSubscription first = new MultipleAssignmentSubscription(); final MultipleAssignmentSubscription mas = new MultipleAssignmentSubscription(); - // tasks.add(mas); // Needs a removal without unsubscription + mas.set(first); + tasks.add(mas); + final Subscription removeMas = Subscriptions.create(new Action0() { + @Override + public void call() { + tasks.remove(mas); + } + }); - try { - Future f = service.schedule(new Runnable() { - @Override - public void run() { - if (mas.isUnsubscribed()) { - return; - } - mas.set(schedule(action)); - // tasks.delete(mas); // Needs a removal without unsubscription + ScheduledAction ea = new ScheduledAction(new Action0() { + @Override + public void call() { + if (mas.isUnsubscribed()) { + return; } - }, delayTime, unit); - mas.set(Subscriptions.from(f)); + // schedule the real action untimed + Subscription s2 = schedule(action); + mas.set(s2); + // unless the worker is unsubscribed, we should get a new ScheduledAction + if (s2.getClass() == ScheduledAction.class) { + // when this ScheduledAction completes, we need to remove the + // MAS referencing the whole setup to avoid leaks + ((ScheduledAction)s2).add(removeMas); + } + } + }); + // This will make sure if ea.call() gets executed before this line + // we don't override the current task in mas. + first.set(ea); + // we don't need to add ea to tasks because it will be tracked through mas/first + + + try { + Future f = service.schedule(ea, delayTime, unit); + ea.add(f); } catch (RejectedExecutionException t) { // report the rejection to plugins RxJavaPlugins.getInstance().getErrorHandler().handleError(t); throw t; } - return mas; + /* + * This allows cancelling either the delayed schedule or the actual schedule referenced + * by mas and makes sure mas is removed from the tasks composite to avoid leaks. + */ + return removeMas; } @Override @@ -150,46 +176,4 @@ public void unsubscribe() { } } - - /** Runs the actual action and maintains an unsubscription state. */ - static final class ExecutorAction implements Runnable, Subscription { - final Action0 actual; - final CompositeSubscription parent; - volatile int unsubscribed; - static final AtomicIntegerFieldUpdater UNSUBSCRIBED_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(ExecutorAction.class, "unsubscribed"); - - public ExecutorAction(Action0 actual, CompositeSubscription parent) { - this.actual = actual; - this.parent = parent; - } - - @Override - public void run() { - if (isUnsubscribed()) { - return; - } - try { - actual.call(); - } catch (Throwable t) { - RxJavaPlugins.getInstance().getErrorHandler().handleError(t); - Thread thread = Thread.currentThread(); - thread.getUncaughtExceptionHandler().uncaughtException(thread, t); - } finally { - unsubscribe(); - } - } - @Override - public boolean isUnsubscribed() { - return unsubscribed != 0; - } - - @Override - public void unsubscribe() { - if (UNSUBSCRIBED_UPDATER.compareAndSet(this, 0, 1)) { - parent.remove(this); - } - } - - } } diff --git a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java index 9fcd303807..b11f7879e1 100644 --- a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java +++ b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java @@ -15,12 +15,20 @@ */ package rx.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.Scheduler; -import rx.internal.util.RxThreadFactory; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import rx.*; +import rx.Scheduler.Worker; +import rx.functions.*; +import rx.internal.schedulers.NewThreadWorker; +import rx.internal.util.RxThreadFactory; +import rx.schedulers.ExecutorScheduler.ExecutorSchedulerWorker; public class ExecutorSchedulerTest extends AbstractSchedulerConcurrencyTests { @@ -40,4 +48,160 @@ public final void testUnhandledErrorIsDeliveredToThreadHandler() throws Interrup public final void testHandledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { SchedulerTests.testHandledErrorIsNotDeliveredToThreadHandler(getScheduler()); } + @Test(timeout = 30000) + public void testCancelledTaskRetention() 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); + + Scheduler.Worker w = Schedulers.io().createWorker(); + for (int i = 0; i < 500000; 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)); + } + } + + /** A simple executor which queues tasks and executes them one-by-one if executeOne() is called. */ + static final class TestExecutor implements Executor { + final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); + @Override + public void execute(Runnable command) { + queue.offer(command); + } + public void executeOne() { + Runnable r = queue.poll(); + if (r != null) { + r.run(); + } + } + public void executeAll() { + Runnable r; + while ((r = queue.poll()) != null) { + r.run(); + } + } + } + + @Test + public void testCancelledTasksDontRun() { + final AtomicInteger calls = new AtomicInteger(); + Action0 task = new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec); + Worker w = custom.createWorker(); + try { + Subscription s1 = w.schedule(task); + Subscription s2 = w.schedule(task); + Subscription s3 = w.schedule(task); + + s1.unsubscribe(); + s2.unsubscribe(); + s3.unsubscribe(); + + exec.executeAll(); + + assertEquals(0, calls.get()); + } finally { + w.unsubscribe(); + } + } + @Test + public void testCancelledWorkerDoesntRunTasks() { + final AtomicInteger calls = new AtomicInteger(); + Action0 task = new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec); + Worker w = custom.createWorker(); + try { + w.schedule(task); + w.schedule(task); + w.schedule(task); + } finally { + w.unsubscribe(); + } + exec.executeAll(); + assertEquals(0, calls.get()); + } + @Test + public void testNoTimedTaskAfterScheduleRetention() throws InterruptedException { + Executor e = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + ExecutorSchedulerWorker w = (ExecutorSchedulerWorker)Schedulers.from(e).createWorker(); + + w.schedule(Actions.empty(), 1, TimeUnit.MILLISECONDS); + + assertTrue(w.tasks.hasSubscriptions()); + + Thread.sleep(100); + + assertFalse(w.tasks.hasSubscriptions()); + } + + @Test + public void testNoTimedTaskPartRetention() { + Executor e = new Executor() { + @Override + public void execute(Runnable command) { + + } + }; + ExecutorSchedulerWorker w = (ExecutorSchedulerWorker)Schedulers.from(e).createWorker(); + + Subscription s = w.schedule(Actions.empty(), 1, TimeUnit.DAYS); + + assertTrue(w.tasks.hasSubscriptions()); + + s.unsubscribe(); + + assertFalse(w.tasks.hasSubscriptions()); + } } From b28007ee8f236ffbad6d1fc21ff5867e14e3d75b Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 6 May 2015 08:25:41 +0200 Subject: [PATCH 252/857] Fix termination race condition in OperatorPublish.dispatch --- .../rx/internal/operators/OperatorPublish.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 0fca3d6c64..662d093ebf 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -458,6 +458,15 @@ void dispatch() { boolean skipFinal = false; try { for (;;) { + /* + * We need to read terminalEvent before checking the queue for emptyness 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 + * have produced elements and set the terminalEvent and we'd quit emitting + * prematurely. + */ + Object term = terminalEvent; /* * See if the queue is empty; since we need this information multiple * times later on, we read it one. @@ -468,7 +477,7 @@ void dispatch() { // if the queue is empty and the terminal event was received, quit // and don't bother restoring emitting to false: no further activity is // possible at this point - if (checkTerminated(terminalEvent, empty)) { + if (checkTerminated(term, empty)) { skipFinal = true; return; } @@ -508,10 +517,11 @@ void dispatch() { // it may happen everyone has unsubscribed between here and producers.get() // or we have no subscribers at all to begin with if (len == unsubscribed) { + term = terminalEvent; // so let's consume a value from the queue Object v = queue.poll(); // or terminate if there was a terminal event and the queue is empty - if (checkTerminated(terminalEvent, v == null)) { + if (checkTerminated(term, v == null)) { skipFinal = true; return; } @@ -748,4 +758,4 @@ public void unsubscribe() { } } } -} +} \ No newline at end of file From 69d6aaf61ed65775496ef15fea0e920c801938a1 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 6 May 2015 13:23:36 +0200 Subject: [PATCH 253/857] Test that finds a TestScheduler bug --- .../java/rx/subjects/TestSubjectTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/test/java/rx/subjects/TestSubjectTest.java diff --git a/src/test/java/rx/subjects/TestSubjectTest.java b/src/test/java/rx/subjects/TestSubjectTest.java new file mode 100644 index 0000000000..2b09235153 --- /dev/null +++ b/src/test/java/rx/subjects/TestSubjectTest.java @@ -0,0 +1,58 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.subjects; + +import org.junit.Test; +import rx.Observer; +import rx.schedulers.TestScheduler; + +import java.util.concurrent.TimeUnit; + +import static org.mockito.Mockito.*; + +public class TestSubjectTest { + + @Test + public void testObserverPropagateValueAfterTriggeringActions() { + final TestScheduler scheduler = new TestScheduler(); + + final TestSubject subject = TestSubject.create(scheduler); + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + subject.subscribe(observer); + + subject.onNext(1); + scheduler.triggerActions(); + + verify(observer, times(1)).onNext(1); + } + + @Test + public void testObserverPropagateValueInFutureTimeAfterTriggeringActions() { + final TestScheduler scheduler = new TestScheduler(); + scheduler.advanceTimeTo(100, TimeUnit.SECONDS); + + final TestSubject subject = TestSubject.create(scheduler); + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + subject.subscribe(observer); + + subject.onNext(1); + scheduler.triggerActions(); + + verify(observer, times(1)).onNext(1); + } +} From 2b4fc37b144847684ec99e1bf319863d5238ef9d Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 6 May 2015 13:25:29 +0200 Subject: [PATCH 254/857] Fixed TestSubject bug in onNext --- src/main/java/rx/subjects/TestSubject.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/rx/subjects/TestSubject.java b/src/main/java/rx/subjects/TestSubject.java index 2400e929f1..bf0db074bf 100644 --- a/src/main/java/rx/subjects/TestSubject.java +++ b/src/main/java/rx/subjects/TestSubject.java @@ -15,8 +15,6 @@ */ package rx.subjects; -import java.util.concurrent.TimeUnit; - import rx.Observer; import rx.Scheduler; import rx.functions.Action0; @@ -25,6 +23,8 @@ import rx.schedulers.TestScheduler; import rx.subjects.SubjectSubscriptionManager.SubjectObserver; +import java.util.concurrent.TimeUnit; + /** * A variety of Subject that is useful for testing purposes. It operates on a {@link TestScheduler} and allows * you to precisely time emissions and notifications to the Subject's subscribers using relative virtual time @@ -136,11 +136,11 @@ public void call() { } /** - * Schedule a call to {@code onNext} at relative time of "now()" on TestScheduler. + * Schedule a call to {@code onNext} on TestScheduler. */ @Override public void onNext(T v) { - onNext(v, innerScheduler.now()); + onNext(v, 0); } private void _onNext(T v) { @@ -154,10 +154,10 @@ private void _onNext(T v) { * * @param v * the item to emit - * @param timeInMilliseconds + * @param delayTime * the number of milliseconds in the future relative to "now()" at which to call {@code onNext} */ - public void onNext(final T v, long timeInMilliseconds) { + public void onNext(final T v, long delayTime) { innerScheduler.schedule(new Action0() { @Override @@ -165,7 +165,7 @@ public void call() { _onNext(v); } - }, timeInMilliseconds, TimeUnit.MILLISECONDS); + }, delayTime, TimeUnit.MILLISECONDS); } @Override From da6ef514458305b2c26acc910175c336700b4636 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 6 May 2015 15:09:52 +0200 Subject: [PATCH 255/857] Fixed TestSubject bug in onError and onCompleted --- src/main/java/rx/subjects/TestSubject.java | 20 +++--- .../java/rx/subjects/TestSubjectTest.java | 69 +++++++++++++++++++ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/subjects/TestSubject.java b/src/main/java/rx/subjects/TestSubject.java index bf0db074bf..2de860c602 100644 --- a/src/main/java/rx/subjects/TestSubject.java +++ b/src/main/java/rx/subjects/TestSubject.java @@ -68,11 +68,11 @@ protected TestSubject(OnSubscribe onSubscribe, SubjectSubscriptionManager } /** - * Schedule a call to {@code onCompleted} at relative time of "now()" on TestScheduler. + * Schedule a call to {@code onCompleted} on TestScheduler. */ @Override public void onCompleted() { - onCompleted(innerScheduler.now()); + onCompleted(0); } private void _onCompleted() { @@ -86,10 +86,10 @@ private void _onCompleted() { /** * Schedule a call to {@code onCompleted} relative to "now()" +n milliseconds in the future. * - * @param timeInMilliseconds + * @param delayTime * the number of milliseconds in the future relative to "now()" at which to call {@code onCompleted} */ - public void onCompleted(long timeInMilliseconds) { + public void onCompleted(long delayTime) { innerScheduler.schedule(new Action0() { @Override @@ -97,15 +97,15 @@ public void call() { _onCompleted(); } - }, timeInMilliseconds, TimeUnit.MILLISECONDS); + }, delayTime, TimeUnit.MILLISECONDS); } /** - * Schedule a call to {@code onError} at relative time of "now()" on TestScheduler. + * Schedule a call to {@code onError} on TestScheduler. */ @Override public void onError(final Throwable e) { - onError(e, innerScheduler.now()); + onError(e, 0); } private void _onError(final Throwable e) { @@ -121,10 +121,10 @@ private void _onError(final Throwable e) { * * @param e * the {@code Throwable} to pass to the {@code onError} method - * @param timeInMilliseconds + * @param dalayTime * the number of milliseconds in the future relative to "now()" at which to call {@code onError} */ - public void onError(final Throwable e, long timeInMilliseconds) { + public void onError(final Throwable e, long dalayTime) { innerScheduler.schedule(new Action0() { @Override @@ -132,7 +132,7 @@ public void call() { _onError(e); } - }, timeInMilliseconds, TimeUnit.MILLISECONDS); + }, dalayTime, TimeUnit.MILLISECONDS); } /** diff --git a/src/test/java/rx/subjects/TestSubjectTest.java b/src/test/java/rx/subjects/TestSubjectTest.java index 2b09235153..dcd54b51c6 100644 --- a/src/test/java/rx/subjects/TestSubjectTest.java +++ b/src/test/java/rx/subjects/TestSubjectTest.java @@ -19,6 +19,7 @@ import rx.Observer; import rx.schedulers.TestScheduler; +import java.io.IOException; import java.util.concurrent.TimeUnit; import static org.mockito.Mockito.*; @@ -55,4 +56,72 @@ public void testObserverPropagateValueInFutureTimeAfterTriggeringActions() { verify(observer, times(1)).onNext(1); } + + + + @Test + public void testObserverPropagateErrorAfterTriggeringActions() { + final IOException e = new IOException(); + final TestScheduler scheduler = new TestScheduler(); + + final TestSubject subject = TestSubject.create(scheduler); + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + subject.subscribe(observer); + + subject.onError(e); + scheduler.triggerActions(); + + verify(observer, times(1)).onError(e); + } + + @Test + public void testObserverPropagateErrorInFutureTimeAfterTriggeringActions() { + final IOException e = new IOException(); + final TestScheduler scheduler = new TestScheduler(); + scheduler.advanceTimeTo(100, TimeUnit.SECONDS); + + final TestSubject subject = TestSubject.create(scheduler); + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + subject.subscribe(observer); + + subject.onError(e); + scheduler.triggerActions(); + + verify(observer, times(1)).onError(e); + } + + + + @Test + public void testObserverPropagateCompletedAfterTriggeringActions() { + final TestScheduler scheduler = new TestScheduler(); + + final TestSubject subject = TestSubject.create(scheduler); + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + subject.subscribe(observer); + + subject.onCompleted(); + scheduler.triggerActions(); + + verify(observer, times(1)).onCompleted(); + } + + @Test + public void testObserverPropagateCompletedInFutureTimeAfterTriggeringActions() { + final TestScheduler scheduler = new TestScheduler(); + scheduler.advanceTimeTo(100, TimeUnit.SECONDS); + + final TestSubject subject = TestSubject.create(scheduler); + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + subject.subscribe(observer); + + subject.onCompleted(); + scheduler.triggerActions(); + + verify(observer, times(1)).onCompleted(); + } } From fa0dba2ae78ccc79e4d7e3434c577384a38a58d5 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 30 Apr 2015 16:10:54 +1000 Subject: [PATCH 256/857] add code quality plugins to build.gradle (jacoco, findbugs) --- build.gradle | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/build.gradle b/build.gradle index 412ccb14d2..303d0ab8f6 100644 --- a/build.gradle +++ b/build.gradle @@ -7,12 +7,49 @@ description = 'RxJava: Reactive Extensions for the JVM – a library for composi apply plugin: 'rxjava-project' apply plugin: 'java' +apply plugin: 'findbugs' +apply plugin: 'jacoco' dependencies { testCompile 'junit:junit-dep:4.10' testCompile 'org.mockito:mockito-core:1.8.5' } +//////////////////////////////////////////////////////////////////// +// to run findbugs: +// ./gradlew check +// then open build/reports/findbugs/main.html +//////////////////////////////////////////////////////////////////// + +findbugs { + ignoreFailures = true + toolVersion = "+" + sourceSets = [sourceSets.main] + reportsDir = file("$project.buildDir/reports/findbugs") + effort = "max" +} + +////////////////////////////////////////////////////////////////// +// to run jacoco: +// ./gradlew test jacocoTestReport +// to run jacoco on a single test (matches OperatorRetry to OperatorRetryTest in test code base): +// ./gradlew -Dtest.single=OperatorRetry test jacocoTestReport +// then open build/reports/jacoco/index.html +///////////////////////////////////////////////////////////////// + +jacoco { + toolVersion = "+" + reportsDir = file("$buildDir/customJacocoReportDir") +} + +jacocoTestReport { + reports { + xml.enabled false + csv.enabled false + html.destination "${buildDir}/reports/jacoco" + } +} + javadoc { exclude "**/rx/internal/**" } @@ -30,3 +67,10 @@ if (project.hasProperty('release.useLastTag')) { test{ maxHeapSize = "2g" } + +tasks.withType(FindBugs) { + reports { + xml.enabled = false + html.enabled = true + } + } From 5d23e29a6a6479bf9992515b82741296f9e45a86 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 7 May 2015 12:33:36 +0200 Subject: [PATCH 257/857] Fixed Observable.combineLatest overflow bug on Android RxRingBuffer size is not a constant and on Android is less then 128 (16). So it causing silent issues when there were given 16 < Observers < 128. --- .../rx/internal/operators/OnSubscribeCombineLatest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java index 1537f91aaf..953895af32 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java @@ -49,10 +49,10 @@ public final class OnSubscribeCombineLatest implements OnSubscribe { public OnSubscribeCombineLatest(List> sources, FuncN combinator) { this.sources = sources; this.combinator = combinator; - if (sources.size() > 128) { - // For design simplicity this is limited to 128. If more are really needed we'll need to adjust - // the design of how RxRingBuffer is used in the implementation below. - throw new IllegalArgumentException("More than 128 sources to combineLatest is not supported."); + if (sources.size() > RxRingBuffer.SIZE) { + // For design simplicity this is limited to RxRingBuffer.SIZE. If more are really needed we'll need to + // adjust the design of how RxRingBuffer is used in the implementation below. + throw new IllegalArgumentException("More than RxRingBuffer.SIZE sources to combineLatest is not supported."); } } From 419dc0fa188ac3337db83bfd702809c3d11ada83 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 8 May 2015 13:23:11 +0200 Subject: [PATCH 258/857] Fixes another race between terminalEvent and the queue being empty. --- src/main/java/rx/internal/operators/OperatorPublish.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 662d093ebf..c6739927ee 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -535,10 +535,11 @@ void dispatch() { // may contain less than requested int d = 0; while (d < maxRequested) { + term = terminalEvent; Object v = queue.poll(); empty = v == null; // let's check if there is a terminal event and the queue became empty just now - if (checkTerminated(terminalEvent, empty)) { + if (checkTerminated(term, empty)) { skipFinal = true; return; } From c28a1c7f0efd7429d55b14474281ded9ae77e14f Mon Sep 17 00:00:00 2001 From: Tomasz Rozbicki Date: Sat, 9 May 2015 13:56:20 +0200 Subject: [PATCH 259/857] Remove unnecessary localHasValue check Due to !hasValue check localHasValue field is always true --- .../operators/OperatorDebounceWithTime.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java index d8d0089441..45d3f14cd9 100644 --- a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java @@ -116,26 +116,22 @@ public synchronized int next(T value) { } public void emit(int index, Subscriber onNextAndComplete, Subscriber onError) { T localValue; - boolean localHasValue; synchronized (this) { if (emitting || !hasValue || index != this.index) { return; } localValue = value; - localHasValue = hasValue; value = null; hasValue = false; emitting = true; } - if (localHasValue) { - try { - onNextAndComplete.onNext(localValue); - } catch (Throwable e) { - onError.onError(e); - return; - } + try { + onNextAndComplete.onNext(localValue); + } catch (Throwable e) { + onError.onError(e); + return; } // Check if a termination was requested in the meantime. From 8eb6482ca7e343a4ab76f5daafb1866878337182 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 1 May 2015 10:04:00 +1000 Subject: [PATCH 260/857] fix OperatorObserveOn race condition where onComplete could be emitted despite onError being called --- .../internal/operators/OperatorObserveOn.java | 89 ++++++++++--------- .../rx/operators/OperatorObserveOnPerf.java | 21 +++++ 2 files changed, 69 insertions(+), 41 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 13a78ca14c..ae013f9d3a 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -75,15 +75,19 @@ private static final class ObserveOnSubscriber extends Subscriber { final NotificationLite on = NotificationLite.instance(); final Queue queue; - volatile boolean completed = false; - volatile boolean failure = false; + + // the status of the current stream + volatile boolean finished = false; + @SuppressWarnings("unused") volatile long requested = 0; + @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "requested"); @SuppressWarnings("unused") volatile long counter; + @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater COUNTER_UPDATER = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "counter"); @@ -127,7 +131,7 @@ public void onStart() { @Override public void onNext(final T t) { - if (isUnsubscribed() || completed) { + if (isUnsubscribed()) { return; } if (!queue.offer(on.next(t))) { @@ -139,30 +143,23 @@ public void onNext(final T t) { @Override public void onCompleted() { - if (isUnsubscribed() || completed) { + if (isUnsubscribed() || finished) { return; } - if (error != null) { - return; - } - completed = true; + finished = true; schedule(); } @Override public void onError(final Throwable e) { - if (isUnsubscribed() || completed) { - return; - } - if (error != null) { + if (isUnsubscribed() || finished) { return; } error = e; // unsubscribe eagerly since time will pass before the scheduled onError results in an unsubscribe event unsubscribe(); - // mark failure so the polling thread will skip onNext still in the queue - completed = true; - failure = true; + finished = true; + // polling thread should skip any onNext still in the queue schedule(); } @@ -191,41 +188,51 @@ void pollQueue() { */ counter = 1; -// middle: while (!scheduledUnsubscribe.isUnsubscribed()) { - if (failure) { - child.onError(error); - return; - } else { - if (requested == 0 && completed && queue.isEmpty()) { + if (finished) { + // only read volatile error once + Throwable err = error; + if (err != null) { + // clear the queue to enable gc + queue.clear(); + // even if there are onNext in the queue we eagerly notify of error + child.onError(err); + return; + } else if (queue.isEmpty()) { child.onCompleted(); return; } - if (REQUESTED.getAndDecrement(this) != 0) { - Object o = queue.poll(); - if (o == null) { - if (completed) { - if (failure) { - child.onError(error); - } else { - child.onCompleted(); - } + } + if (REQUESTED.getAndDecrement(this) != 0) { + Object o = queue.poll(); + if (o == null) { + // nothing in queue (but be careful, something could be added concurrently right now) + if (finished) { + // only read volatile error once + Throwable err = error; + if (err != null) { + // clear the queue to enable gc + queue.clear(); + // even if there are onNext in the queue we eagerly notify of error + child.onError(err); + return; + } else if (queue.isEmpty()) { + child.onCompleted(); return; - } - // nothing in queue - REQUESTED.incrementAndGet(this); - break; - } else { - if (!on.accept(child, o)) { - // non-terminal event so let's increment count - emitted++; } } - } else { - // we hit the end ... so increment back to 0 again - REQUESTED.incrementAndGet(this); + BackpressureUtils.getAndAddRequest(REQUESTED, this, 1); break; + } else { + if (!on.accept(child, o)) { + // non-terminal event so let's increment count + emitted++; + } } + } else { + // we hit the end ... so increment back to 0 again + BackpressureUtils.getAndAddRequest(REQUESTED, this, 1); + break; } } } while (COUNTER_UPDATER.decrementAndGet(this) > 0); diff --git a/src/perf/java/rx/operators/OperatorObserveOnPerf.java b/src/perf/java/rx/operators/OperatorObserveOnPerf.java index 80feb8ecb6..a105f09548 100644 --- a/src/perf/java/rx/operators/OperatorObserveOnPerf.java +++ b/src/perf/java/rx/operators/OperatorObserveOnPerf.java @@ -66,5 +66,26 @@ public void observeOnImmediate(Input input) throws InterruptedException { input.observable.observeOn(Schedulers.immediate()).subscribe(o); o.latch.await(); } + + @Benchmark + public void observeOnComputationSubscribedOnComputation(Input input) throws InterruptedException { + LatchedObserver o = input.newLatchedObserver(); + input.observable.subscribeOn(Schedulers.computation()).observeOn(Schedulers.computation()).subscribe(o); + o.latch.await(); + } + + @Benchmark + public void observeOnNewThreadSubscribedOnComputation(Input input) throws InterruptedException { + LatchedObserver o = input.newLatchedObserver(); + input.observable.subscribeOn(Schedulers.computation()).observeOn(Schedulers.newThread()).subscribe(o); + o.latch.await(); + } + + @Benchmark + public void observeOnImmediateSubscribedOnComputation(Input input) throws InterruptedException { + LatchedObserver o = input.newLatchedObserver(); + input.observable.subscribeOn(Schedulers.computation()).observeOn(Schedulers.immediate()).subscribe(o); + o.latch.await(); + } } From 18caf0e77ae1f7323095c7bd2fa93464a8146368 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sun, 10 May 2015 07:49:37 +1000 Subject: [PATCH 261/857] use new pollQueue from @akarnokd --- .../internal/operators/OperatorObserveOn.java | 60 +++++++------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index ae013f9d3a..08f4088ceb 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -182,62 +182,42 @@ protected void schedule() { void pollQueue() { int emitted = 0; do { - /* - * Set to 1 otherwise it could have grown very large while in the last poll loop - * and then we can end up looping all those times again here before exiting even once we've drained - */ counter = 1; - - while (!scheduledUnsubscribe.isUnsubscribed()) { + long produced = 0; + long r = requested; + while (!child.isUnsubscribed()) { + Throwable error; if (finished) { - // only read volatile error once - Throwable err = error; - if (err != null) { - // clear the queue to enable gc + if ((error = this.error) != null) { + // errors shortcut the queue so + // release the elements in the queue for gc queue.clear(); - // even if there are onNext in the queue we eagerly notify of error - child.onError(err); + child.onError(error); return; - } else if (queue.isEmpty()) { + } else + if (queue.isEmpty()) { child.onCompleted(); return; } } - if (REQUESTED.getAndDecrement(this) != 0) { + if (r > 0) { Object o = queue.poll(); - if (o == null) { - // nothing in queue (but be careful, something could be added concurrently right now) - if (finished) { - // only read volatile error once - Throwable err = error; - if (err != null) { - // clear the queue to enable gc - queue.clear(); - // even if there are onNext in the queue we eagerly notify of error - child.onError(err); - return; - } else if (queue.isEmpty()) { - child.onCompleted(); - return; - } - } - BackpressureUtils.getAndAddRequest(REQUESTED, this, 1); - break; + if (o != null) { + child.onNext(on.getValue(o)); + r--; + emitted++; + produced++; } else { - if (!on.accept(child, o)) { - // non-terminal event so let's increment count - emitted++; - } + break; } } else { - // we hit the end ... so increment back to 0 again - BackpressureUtils.getAndAddRequest(REQUESTED, this, 1); break; } } + if (produced > 0) { + REQUESTED.addAndGet(this, -produced); + } } while (COUNTER_UPDATER.decrementAndGet(this) > 0); - - // request the number of items that we emitted in this poll loop if (emitted > 0) { request(emitted); } From c9d932420cb68e4c9703224cdfefc51cdcb161b8 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 13 May 2015 16:28:14 +1000 Subject: [PATCH 262/857] don't reduce requested by produced if requested is Long.MAX_VALUE --- src/main/java/rx/internal/operators/OperatorObserveOn.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 08f4088ceb..e15c2f93cf 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -214,7 +214,7 @@ void pollQueue() { break; } } - if (produced > 0) { + if (produced > 0 && requested != Long.MAX_VALUE) { REQUESTED.addAndGet(this, -produced); } } while (COUNTER_UPDATER.decrementAndGet(this) > 0); From ff5001beb1c912a1acaf9d67c6135bb3c59247bb Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 14 May 2015 08:31:06 +0200 Subject: [PATCH 263/857] More assertions for TestSubscriber --- .../java/rx/observers/TestSubscriber.java | 202 ++++++++++++++++-- 1 file changed, 180 insertions(+), 22 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 709027ea97..611f1e1137 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -15,10 +15,13 @@ */ package rx.observers; -import java.util.List; +import java.util.*; import java.util.concurrent.*; import rx.*; +import rx.Observer; +import rx.annotations.Experimental; +import rx.exceptions.CompositeException; /** * A {@code TestSubscriber} is a variety of {@link Subscriber} that you can use for unit testing, to perform @@ -29,34 +32,72 @@ 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() { - public TestSubscriber(Subscriber delegate) { + @Override + public void onCompleted() { + // do nothing + } + + @Override + public void onError(Throwable e) { + // do nothing + } + + @Override + public void onNext(Object t) { + // do nothing + } + + }; + + /** + * Constructs a TestSubscriber with the initial request to be requested from upstream. + * @param initialRequest the initial request value, negative value will revert to the default unbounded behavior + */ + @SuppressWarnings("unchecked") + @Experimental + public TestSubscriber(long initialRequest) { + this((Observer)INERT, initialRequest); + } + + /** + * Constructs a TestSubscriber with the initial request to be requested from upstream + * and a delegate Observer to wrap. + * @param initialRequest the initial request value, negative value will revert to the default unbounded behavior + * @param delegate the Observer instance to wrap + */ + @Experimental + public TestSubscriber(Observer delegate, long initialRequest) { + if (delegate == null) { + throw new NullPointerException(); + } this.testObserver = new TestObserver(delegate); + this.initialRequest = initialRequest; + } + + public TestSubscriber(Subscriber delegate) { + this(delegate, -1); } public TestSubscriber(Observer delegate) { - this.testObserver = new TestObserver(delegate); + this(delegate, -1); } public TestSubscriber() { - this.testObserver = new TestObserver(new Observer() { - - @Override - public void onCompleted() { - // do nothing - } - - @Override - public void onError(Throwable e) { - // do nothing - } - - @Override - public void onNext(T t) { - // do nothing - } - - }); + this(-1); + } + + @Override + public void onStart() { + if (initialRequest >= 0) { + requestMore(initialRequest); + } else { + super.onStart(); + } } /** @@ -261,4 +302,121 @@ public void awaitTerminalEventAndUnsubscribeOnTimeout(long timeout, TimeUnit uni public Thread getLastSeenThread() { return lastSeenThread; } -} + + /** + * Assert if there is exactly a single completion event. + */ + @Experimental + public void assertCompleted() { + int s = testObserver.getOnCompletedEvents().size(); + if (s == 0) { + throw new AssertionError("Not completed!"); + } else + if (s > 1) { + throw new AssertionError("Completed multiple times: " + s); + } + } + /** + * Assert if there is no completion event. + */ + @Experimental + public void assertNotCompleted() { + int s = testObserver.getOnCompletedEvents().size(); + if (s == 1) { + throw new AssertionError("Completed!"); + } else + if (s > 1) { + throw new AssertionError("Completed multiple times: " + s); + } + } + /** + * Assert if there is exactly one error event which is a subclass of the given class. + * @param clazz the class to check the error against. + */ + @Experimental + public void assertError(Class clazz) { + List err = testObserver.getOnErrorEvents(); + if (err.size() == 0) { + throw new AssertionError("No errors"); + } else + if (err.size() > 1) { + throw new AssertionError("Multiple errors: " + err.size(), new CompositeException(err)); + } else + if (!clazz.isInstance(err.get(0))) { + throw new AssertionError("Exceptions differ; expected: " + clazz + ", actual: " + err.get(0), err.get(0)); + } + } + /** + * Assert there is a single onError event with the exact exception. + * @param throwable the throwable to check + */ + @Experimental + public void assertError(Throwable throwable) { + List err = testObserver.getOnErrorEvents(); + if (err.size() == 0) { + throw new AssertionError("No errors"); + } else + if (err.size() > 1) { + throw new AssertionError("Multiple errors: " + err.size(), new CompositeException(err)); + } else + if (throwable.equals(err.get(0))) { + throw new AssertionError("Exceptions differ; expected: " + throwable + ", actual: " + err.get(0), err.get(0)); + } + } + /** + * Assert for no onError and onCompleted events. + */ + @Experimental + public void assertNoTerminalEvent() { + List err = testObserver.getOnErrorEvents(); + int s = testObserver.getOnCompletedEvents().size(); + if (err.size() > 0 || s > 0) { + if (err.isEmpty()) { + throw new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none"); + } else + if (err.size() == 1) { + throw new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none", err.get(0)); + } else { + throw new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none", new CompositeException(err)); + } + } + } + /** + * Assert if there are no onNext events received. + */ + @Experimental + public void assertNoValues() { + int s = testObserver.getOnNextEvents().size(); + if (s > 0) { + throw new AssertionError("No onNext events expected yet some received: " + s); + } + } + /** + * Assert if the given number of onNext events are received. + * @param count the expected number of onNext events + */ + @Experimental + public void assertValueCount(int count) { + int s = testObserver.getOnNextEvents().size(); + if (s != count) { + throw new AssertionError("Number of onNext events differ; expected: " + count + ", actual: " + s); + } + } + + /** + * Assert if the received onNext events, in order, are the specified values. + * @param values the values to check + */ + @Experimental + public void assertValues(T... values) { + assertReceivedOnNext(Arrays.asList(values)); + } + /** + * Assert if there is only a single received onNext event. + * @param values the values to check + */ + @Experimental + public void assertValue(T value) { + assertReceivedOnNext(Collections.singletonList(value)); + } +} \ No newline at end of file From e7c73e85c5fd561517a17e8284d84117146fee9d Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 14 May 2015 22:17:37 +1000 Subject: [PATCH 264/857] remove findbugs and jacoco because are run with default build, will figure out later --- build.gradle | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/build.gradle b/build.gradle index 35637a7670..8b934db6eb 100644 --- a/build.gradle +++ b/build.gradle @@ -7,49 +7,12 @@ description = 'RxJava: Reactive Extensions for the JVM – a library for composi apply plugin: 'rxjava-project' apply plugin: 'java' -apply plugin: 'findbugs' -apply plugin: 'jacoco' dependencies { testCompile 'junit:junit-dep:4.10' testCompile 'org.mockito:mockito-core:1.8.5' } -//////////////////////////////////////////////////////////////////// -// to run findbugs: -// ./gradlew check -// then open build/reports/findbugs/main.html -//////////////////////////////////////////////////////////////////// - -findbugs { - ignoreFailures = true - toolVersion = "+" - sourceSets = [sourceSets.main] - reportsDir = file("$project.buildDir/reports/findbugs") - effort = "max" -} - -////////////////////////////////////////////////////////////////// -// to run jacoco: -// ./gradlew test jacocoTestReport -// to run jacoco on a single test (matches OperatorRetry to OperatorRetryTest in test code base): -// ./gradlew -Dtest.single=OperatorRetry test jacocoTestReport -// then open build/reports/jacoco/index.html -///////////////////////////////////////////////////////////////// - -jacoco { - toolVersion = "+" - reportsDir = file("$buildDir/customJacocoReportDir") -} - -jacocoTestReport { - reports { - xml.enabled false - csv.enabled false - html.destination "${buildDir}/reports/jacoco" - } -} - javadoc { exclude "**/rx/internal/**" } @@ -67,10 +30,3 @@ if (project.hasProperty('release.useLastTag')) { test{ maxHeapSize = "2g" } - -tasks.withType(FindBugs) { - reports { - xml.enabled = false - html.enabled = true - } - } From 24ca4f78be9e8149f4d060148514d66bbb41e8e2 Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 14 May 2015 12:46:46 -0700 Subject: [PATCH 265/857] Javadocs: adding marble diagram, return value for onBackpressureLatest --- src/main/java/rx/Observable.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index ef05674674..919548fd32 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5370,7 +5370,9 @@ public final Observable onBackpressureBlock() { * Instructs an Observable that is emitting items faster than its observer can consume them to * hold onto the latest value and emit that on request. *

- * Its behavior is logically equivalent to toBlocking().latest() with the exception that + * + *

+ * Its behavior is logically equivalent to {@code toBlocking().latest()} with the exception that * the downstream is not blocking while requesting more values. *

* Note that if the upstream Observable does support backpressure, this operator ignores that capability @@ -5378,7 +5380,8 @@ public final Observable onBackpressureBlock() { *

* Note that due to the nature of how backpressure requests are propagated through subscribeOn/observeOn, * requesting more than 1 from downstream doesn't guarantee a continuous delivery of onNext events. - * @return + * + * @return the source Observable modified so that it emits the most recently-received item upon request * @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) */ From 1236f07d7d802758f9bcc7dcf87a2df166d7e32c Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 15 May 2015 10:56:49 +1000 Subject: [PATCH 266/857] add request overflow checks and prevent Long.MAX_VALUE requests being decremented in OperatorGroupBy, added unit test that failed with previous code --- .../internal/operators/OperatorGroupBy.java | 16 +++++-- .../operators/OperatorGroupByTest.java | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index ef548066f3..93631569df 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -194,7 +194,7 @@ public void onError(Throwable e) { // If we already have items queued when a request comes in we vend those and decrement the outstanding request count void requestFromGroupedObservable(long n, GroupState group) { - group.requested.getAndAdd(n); + BackpressureUtils.getAndAddRequest(group.requested, n); if (group.count.getAndIncrement() == 0) { pollQueue(group); } @@ -330,13 +330,19 @@ private void cleanupGroup(Object key) { private void emitItem(GroupState groupState, Object item) { Queue q = groupState.buffer; AtomicLong keyRequested = groupState.requested; + //don't need to check for requested being Long.MAX_VALUE because this + //field is capped at MAX_QUEUE_SIZE REQUESTED.decrementAndGet(this); // short circuit buffering if (keyRequested != null && keyRequested.get() > 0 && (q == null || q.isEmpty())) { @SuppressWarnings("unchecked") Observer obs = (Observer)groupState.getObserver(); nl.accept(obs, item); - keyRequested.decrementAndGet(); + if (keyRequested.get() != Long.MAX_VALUE) { + // best endeavours check (no CAS loop here) because we mainly care about + // the initial request being Long.MAX_VALUE and that value being conserved. + keyRequested.decrementAndGet(); + } } else { q.add(item); BUFFERED_COUNT.incrementAndGet(this); @@ -381,7 +387,11 @@ private void drainIfPossible(GroupState groupState) { @SuppressWarnings("unchecked") Observer obs = (Observer)groupState.getObserver(); nl.accept(obs, t); - groupState.requested.decrementAndGet(); + if (groupState.requested.get()!=Long.MAX_VALUE) { + // best endeavours check (no CAS loop here) because we mainly care about + // the initial request being Long.MAX_VALUE and that value being conserved. + groupState.requested.decrementAndGet(); + } BUFFERED_COUNT.decrementAndGet(this); // if we have used up all the events we requested from upstream then figure out what to ask for this time based on the empty space in the buffer diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index 42023508d3..b14b7ad373 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -34,6 +34,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -1454,4 +1455,50 @@ public Integer call(Integer i) { assertEquals(Arrays.asList(e), inner1.getOnErrorEvents()); assertEquals(Arrays.asList(e), inner2.getOnErrorEvents()); } + + @Test + public void testRequestOverflow() { + final AtomicBoolean completed = new AtomicBoolean(false); + Observable + .just(1, 2, 3) + // group into one group + .groupBy(new Func1() { + @Override + public Integer call(Integer t) { + return 1; + } + }) + // flatten + .concatMap(new Func1, Observable>() { + @Override + public Observable call(GroupedObservable g) { + return g; + } + }) + .subscribe(new Subscriber() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onCompleted() { + completed.set(true); + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + System.out.println(t); + //provoke possible request overflow + request(Long.MAX_VALUE-1); + }}); + assertTrue(completed.get()); + } } From bad4d40a7b59cb443c3cb19d00ab80000e017a5f Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 15 May 2015 11:48:26 +1000 Subject: [PATCH 267/857] OperatorConcat - use BackpressureUtils to prevent request overflow and add n > 0 check to prevent race condition --- .../rx/internal/operators/OperatorConcat.java | 2 +- .../operators/OperatorConcatTest.java | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index f1a429dea4..8e8514b9ef 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -115,7 +115,7 @@ public void onStart() { private void requestFromChild(long n) { // we track 'requested' so we know whether we should subscribe the next or not ConcatInnerSubscriber actualSubscriber = currentSubscriber; - if (REQUESTED_UPDATER.getAndAdd(this, n) == 0) { + if (n > 0 && BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER, this, n) == 0) { if (actualSubscriber == null && wip > 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 diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 5ad80c6d70..75bfee65f4 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -16,6 +16,7 @@ package rx.internal.operators; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; @@ -35,7 +36,9 @@ import org.mockito.InOrder; import rx.Observable.OnSubscribe; +import rx.Scheduler.Worker; import rx.*; +import rx.functions.Action0; import rx.functions.Func1; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; @@ -766,4 +769,30 @@ public void onError(Throwable e) { assertEquals(n, counter.get()); } + + @Test + public void testRequestOverflowDoesNotStallStream() { + Observable o1 = Observable.just(1,2,3); + Observable o2 = Observable.just(4,5,6); + final AtomicBoolean completed = new AtomicBoolean(false); + o1.concatWith(o2).subscribe(new Subscriber() { + + @Override + public void onCompleted() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(2); + }}); + + assertTrue(completed.get()); + } + } From 7071fc0a84ca840a11ebc6c4be0bba4c8a877e7e Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 16 May 2015 20:28:08 +1000 Subject: [PATCH 268/857] OperatorObserveOn should not request more after child is unsubscribed --- .../internal/operators/OperatorObserveOn.java | 4 +- .../operators/OperatorObserveOnTest.java | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index e15c2f93cf..1f1f380ff0 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -185,7 +185,9 @@ void pollQueue() { counter = 1; long produced = 0; long r = requested; - while (!child.isUnsubscribed()) { + for (;;) { + if (child.isUnsubscribed()) + return; Throwable error; if (finished) { if ((error = this.error) != null) { diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index b0c8a5bcfd..1f0cc0a892 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -26,7 +26,9 @@ 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.List; import java.util.concurrent.CountDownLatch; @@ -765,4 +767,42 @@ public void onNext(Integer t) { } + @Test + public void testNoMoreRequestsAfterUnsubscribe() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final List requests = Collections.synchronizedList(new ArrayList()); + Observable.range(1, 1000000) + .doOnRequest(new Action1() { + + @Override + public void call(Long n) { + requests.add(n); + } + }) + .observeOn(Schedulers.io()) + .subscribe(new Subscriber() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer t) { + unsubscribe(); + latch.countDown(); + } + }); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + assertEquals(1, requests.size()); + } + } From 39be3108711b0d66a2cf8e98737660b6d9ef4208 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 19 May 2015 13:40:00 +0200 Subject: [PATCH 269/857] Standard producers and some more (platform-safe) queues from JCTools --- .../operators/OperatorToObservableList.java | 11 +- .../OperatorToObservableSortedList.java | 3 +- .../operators/SingleDelayedProducer.java | 87 ---- .../internal/producers/ProducerArbiter.java | 191 +++++++++ .../producers/ProducerObserverArbiter.java | 282 +++++++++++++ .../rx/internal/producers/QueuedProducer.java | 187 +++++++++ .../producers/QueuedValueProducer.java | 138 +++++++ .../producers/SingleDelayedProducer.java | 115 ++++++ .../rx/internal/producers/SingleProducer.java | 79 ++++ .../util/atomic/BaseLinkedAtomicQueue.java | 90 +++++ .../internal/util/atomic/LinkedQueueNode.java | 54 +++ .../util/atomic/MpscLinkedAtomicQueue.java | 123 ++++++ .../util/atomic/SpscLinkedAtomicQueue.java | 103 +++++ .../internal/util/unsafe/BaseLinkedQueue.java | 123 ++++++ .../internal/util/unsafe/MpscLinkedQueue.java | 133 ++++++ .../internal/util/unsafe/SpscLinkedQueue.java | 105 +++++ .../rx/internal/util/unsafe/UnsafeAccess.java | 21 + .../rx/internal/producers/ProducersTest.java | 381 ++++++++++++++++++ 18 files changed, 2132 insertions(+), 94 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/SingleDelayedProducer.java create mode 100644 src/main/java/rx/internal/producers/ProducerArbiter.java create mode 100644 src/main/java/rx/internal/producers/ProducerObserverArbiter.java create mode 100644 src/main/java/rx/internal/producers/QueuedProducer.java create mode 100644 src/main/java/rx/internal/producers/QueuedValueProducer.java create mode 100644 src/main/java/rx/internal/producers/SingleDelayedProducer.java create mode 100644 src/main/java/rx/internal/producers/SingleProducer.java create mode 100644 src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java create mode 100644 src/main/java/rx/internal/util/atomic/LinkedQueueNode.java create mode 100644 src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java create mode 100644 src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java create mode 100644 src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java create mode 100644 src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java create mode 100644 src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java create mode 100644 src/test/java/rx/internal/producers/ProducersTest.java diff --git a/src/main/java/rx/internal/operators/OperatorToObservableList.java b/src/main/java/rx/internal/operators/OperatorToObservableList.java index 8d7dff8f96..e77826acc6 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableList.java @@ -15,12 +15,11 @@ */ package rx.internal.operators; -import rx.Observable.Operator; -import rx.Subscriber; +import java.util.*; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; +import rx.Observable.Operator; +import rx.*; +import rx.internal.producers.SingleDelayedProducer; /** * Returns an {@code Observable} that emits a single item, a list composed of all the items emitted by the @@ -90,7 +89,7 @@ public void onCompleted() { return; } list = null; - producer.set(result); + producer.setValue(result); } } diff --git a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java index f2d5cb9948..a3e9c54839 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java @@ -20,6 +20,7 @@ import rx.Observable.Operator; import rx.*; import rx.functions.Func2; +import rx.internal.producers.SingleDelayedProducer; /** * Return an {@code Observable} that emits the items emitted by the source {@code Observable}, in a sorted order @@ -77,7 +78,7 @@ public void onCompleted() { onError(e); return; } - producer.set(a); + producer.setValue(a); } } diff --git a/src/main/java/rx/internal/operators/SingleDelayedProducer.java b/src/main/java/rx/internal/operators/SingleDelayedProducer.java deleted file mode 100644 index 9405250ac5..0000000000 --- a/src/main/java/rx/internal/operators/SingleDelayedProducer.java +++ /dev/null @@ -1,87 +0,0 @@ -package rx.internal.operators; - -import java.util.concurrent.atomic.AtomicInteger; - -import rx.*; - -/** - * A producer that holds a single value until it is requested and emits it followed by an onCompleted. - */ -public final class SingleDelayedProducer extends AtomicInteger implements Producer { - /** */ - private static final long serialVersionUID = 4721551710164477552L; - /** The actual child. */ - final Subscriber child; - /** The value to emit, acquired and released by compareAndSet. */ - T value; - /** State flag: request() called with positive value. */ - static final int REQUESTED = 1; - /** State flag: set() called. */ - static final int SET = 2; - /** - * Constructs a SingleDelayedProducer with the given child as output. - * @param child the subscriber to emit the value and completion events - */ - public SingleDelayedProducer(Subscriber child) { - this.child = child; - } - @Override - public void request(long n) { - if (n > 0) { - for (;;) { - int s = get(); - // if already requested - if ((s & REQUESTED) != 0) { - break; - } - int u = s | REQUESTED; - if (compareAndSet(s, u)) { - if ((s & SET) != 0) { - emit(); - } - break; - } - } - } - } - /** - * Sets the value to be emitted and emits it if there was a request. - * Should be called only once and from a single thread - * @param value the value to set and possibly emit - */ - public void set(T value) { - for (;;) { - int s = get(); - // if already set - if ((s & SET) != 0) { - break; - } - int u = s | SET; - this.value = value; - if (compareAndSet(s, u)) { - if ((s & REQUESTED) != 0) { - emit(); - } - break; - } - } - } - /** - * Emits the set value if the child is not unsubscribed and bounces back - * exceptions caught from child.onNext. - */ - void emit() { - try { - T v = value; - value = null; // do not hold onto the value - if (child.isUnsubscribed()) { - return; - } - child.onNext(v); - } catch (Throwable t) { - child.onError(t); - return; - } - child.onCompleted(); - } -} \ No newline at end of file diff --git a/src/main/java/rx/internal/producers/ProducerArbiter.java b/src/main/java/rx/internal/producers/ProducerArbiter.java new file mode 100644 index 0000000000..d90a575447 --- /dev/null +++ b/src/main/java/rx/internal/producers/ProducerArbiter.java @@ -0,0 +1,191 @@ +/** + * Copyright 2015 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.producers; + +import rx.*; + +/** + * Producer that allows changing an underlying producer atomically and correctly resume with the accumulated + * requests. + */ +public final class ProducerArbiter implements Producer { + long requested; + Producer currentProducer; + + boolean emitting; + long missedRequested; + long missedProduced; + Producer missedProducer; + + static final Producer NULL_PRODUCER = new Producer() { + @Override + public void request(long n) { + + } + }; + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + if (n == 0) { + return; + } + synchronized (this) { + if (emitting) { + missedRequested += n; + return; + } + emitting = true; + } + boolean skipFinal = false; + try { + long r = requested; + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + requested = u; + + Producer p = currentProducer; + if (p != null) { + p.request(n); + } + + emitLoop(); + skipFinal = true; + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + + public void produced(long n) { + if (n <= 0) { + throw new IllegalArgumentException("n > 0 required"); + } + synchronized (this) { + if (emitting) { + missedProduced += n; + return; + } + emitting = true; + } + + boolean skipFinal = false; + try { + long r = requested; + if (r != Long.MAX_VALUE) { + long u = r - n; + if (u < 0) { + throw new IllegalStateException(); + } + requested = u; + } + + emitLoop(); + skipFinal = true; + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + + public void setProducer(Producer newProducer) { + synchronized (this) { + if (emitting) { + missedProducer = newProducer == null ? NULL_PRODUCER : newProducer; + return; + } + emitting = true; + } + boolean skipFinal = false; + try { + currentProducer = newProducer; + if (newProducer != null) { + newProducer.request(requested); + } + + emitLoop(); + skipFinal = true; + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + + public void emitLoop() { + for (;;) { + long localRequested; + long localProduced; + Producer localProducer; + synchronized (this) { + localRequested = missedRequested; + localProduced = missedProduced; + localProducer = missedProducer; + if (localRequested == 0L + && localProduced == 0L + && localProducer == null) { + emitting = false; + return; + } + missedRequested = 0L; + missedProduced = 0L; + missedProducer = null; + } + + long r = requested; + + if (r != Long.MAX_VALUE) { + long u = r + localRequested; + if (u < 0 || u == Long.MAX_VALUE) { + r = Long.MAX_VALUE; + requested = r; + } else { + long v = u - localProduced; + if (v < 0) { + throw new IllegalStateException("more produced than requested"); + } + r = v; + requested = v; + } + } + if (localProducer != null) { + if (localProducer == NULL_PRODUCER) { + currentProducer = null; + } else { + currentProducer = localProducer; + localProducer.request(r); + } + } else { + Producer p = currentProducer; + if (p != null && localRequested != 0L) { + p.request(localRequested); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/producers/ProducerObserverArbiter.java b/src/main/java/rx/internal/producers/ProducerObserverArbiter.java new file mode 100644 index 0000000000..ff059590b5 --- /dev/null +++ b/src/main/java/rx/internal/producers/ProducerObserverArbiter.java @@ -0,0 +1,282 @@ +/** + * Copyright 2015 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.producers; + +import java.util.*; + +import rx.*; +import rx.Observer; +import rx.exceptions.*; + +/** + * Producer that serializes any event emission with requesting and producer changes. + *

+ * The implementation shortcuts on error and overwrites producers that got delayed, similar + * to ProducerArbiter. + * + * @param the value type + */ +public final class ProducerObserverArbiter implements Producer, Observer { + final Subscriber child; + + boolean emitting; + + List queue; + + Producer currentProducer; + long requested; + + long missedRequested; + Producer missedProducer; + Object missedTerminal; + + volatile boolean hasError; + + static final Producer NULL_PRODUCER = new Producer() { + @Override + public void request(long n) { + + } + }; + + public ProducerObserverArbiter(Subscriber child) { + this.child = child; + } + + @Override + public void onNext(T t) { + synchronized (this) { + if (emitting) { + List q = queue; + if (q == null) { + q = new ArrayList(4); + queue = q; + } + q.add(t); + return; + } + } + boolean skipFinal = false; + try { + child.onNext(t); + + long r = requested; + if (r != Long.MAX_VALUE) { + requested = r - 1; + } + + emitLoop(); + skipFinal = true; + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + + @Override + public void onError(Throwable e) { + boolean emit; + synchronized (this) { + if (emitting) { + missedTerminal = e; + emit = false; + } else { + emitting = true; + emit = true; + } + } + if (emit) { + child.onError(e); + } else { + hasError = true; + } + } + + @Override + public void onCompleted() { + synchronized (this) { + if (emitting) { + missedTerminal = true; + return; + } + emitting = true; + } + child.onCompleted(); + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + if (n == 0) { + return; + } + synchronized (this) { + if (emitting) { + missedRequested += n; + return; + } + emitting = true; + } + boolean skipFinal = false; + try { + long r = requested; + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + requested = u; + + Producer p = currentProducer; + if (p != null) { + p.request(n); + } + + emitLoop(); + skipFinal = true; + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + + public void setProducer(Producer p) { + synchronized (this) { + if (emitting) { + missedProducer = p != null ? p : NULL_PRODUCER; + return; + } + emitting = true; + } + boolean skipFinal = false; + try { + currentProducer = p; + long r = requested; + if (p != null && r != 0) { + p.request(r); + } + emitLoop(); + skipFinal = true; + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + + void emitLoop() { + final Subscriber c = child; + + outer: + for (;;) { + long localRequested; + Producer localProducer; + Object localTerminal; + List q; + synchronized (this) { + localRequested = missedRequested; + localProducer = missedProducer; + localTerminal = missedTerminal; + q = queue; + if (localRequested == 0L && localProducer == null && q == null + && localTerminal == null) { + emitting = false; + return; + } + missedRequested = 0L; + missedProducer = null; + queue = null; + missedTerminal = null; + } + boolean empty = q == null || q.isEmpty(); + if (localTerminal != null) { + if (localTerminal != Boolean.TRUE) { + c.onError((Throwable)localTerminal); + return; + } else + if (empty) { + c.onCompleted(); + return; + } + } + long e = 0; + if (q != null) { + for (T v : q) { + if (c.isUnsubscribed()) { + return; + } else + if (hasError) { + continue outer; // if an error has been set, shortcut the loop and act on it + } + try { + c.onNext(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + Throwable ex1 = OnErrorThrowable.addValueAsLastCause(ex, v); + c.onError(ex1); + return; + } + } + e += q.size(); + } + long r = requested; + // if requested is max, we don't do any accounting + if (r != Long.MAX_VALUE) { + // if there were missing requested, add it up + if (localRequested != 0L) { + long u = r + localRequested; + if (u < 0) { + u = Long.MAX_VALUE; + } + r = u; + } + // if there were emissions and we don't run on max since the last check, subtract + if (e != 0L && r != Long.MAX_VALUE) { + long u = r - e; + if (u < 0) { + throw new IllegalStateException("More produced than requested"); + } + r = u; + } + requested = r; + } + if (localProducer != null) { + if (localProducer == NULL_PRODUCER) { + currentProducer = null; + } else { + currentProducer = localProducer; + if (r != 0L) { + localProducer.request(r); + } + } + } else { + Producer p = currentProducer; + if (p != null && localRequested != 0L) { + p.request(localRequested); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/producers/QueuedProducer.java b/src/main/java/rx/internal/producers/QueuedProducer.java new file mode 100644 index 0000000000..8dbf4f361e --- /dev/null +++ b/src/main/java/rx/internal/producers/QueuedProducer.java @@ -0,0 +1,187 @@ +/** + * Copyright 2015 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.producers; + +import java.util.Queue; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.exceptions.*; +import rx.internal.operators.BackpressureUtils; +import rx.internal.util.atomic.SpscLinkedAtomicQueue; +import rx.internal.util.unsafe.*; + +/** + * Producer that holds an unbounded (or custom) queue, handles terminal events, + * enqueues values and relays them to a child subscriber on request. + * + * @param the value type + */ +public final class QueuedProducer extends AtomicLong implements Producer, Observer { + + /** */ + private static final long serialVersionUID = 7277121710709137047L; + + final Subscriber child; + final Queue queue; + final AtomicInteger wip; + + Throwable error; + volatile boolean done; + + static final Object NULL_SENTINEL = new Object(); + + /** + * Constructs an instance with the target child subscriber and an Spsc Linked (Atomic) Queue + * as the queue implementation. + * @param child the target child subscriber + */ + public QueuedProducer(Subscriber child) { + this(child, UnsafeAccess.isUnsafeAvailable() + ? new SpscLinkedQueue() : new SpscLinkedAtomicQueue()); + } + /** + * Constructs an instance with the target child subscriber and a custom queue implementation + * @param child the target child subscriber + * @param queue the queue to use + */ + public QueuedProducer(Subscriber child, Queue queue) { + this.child = child; + this.queue = queue; + this.wip = new AtomicInteger(); + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + if (n > 0) { + BackpressureUtils.getAndAddRequest(this, n); + drain(); + } + } + + /** + * Offers a value to this producer and tries to emit any queud 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 + */ + public boolean offer(T value) { + if (value == null) { + if (!queue.offer(NULL_SENTINEL)) { + return false; + } + } else { + if (!queue.offer(value)) { + return false; + } + } + drain(); + return true; + } + + @Override + public void onNext(T value) { + if (!offer(value)) { + onError(new MissingBackpressureException()); + } + } + + @Override + public void onError(Throwable e) { + error = e; + done = true; + drain(); + } + + @Override + public void onCompleted() { + done = true; + drain(); + } + + private boolean checkTerminated(boolean isDone, + boolean isEmpty) { + if (child.isUnsubscribed()) { + return true; + } + if (isDone) { + Throwable e = error; + if (e != null) { + queue.clear(); + child.onError(e); + return true; + } else + if (isEmpty) { + child.onCompleted(); + return true; + } + } + return false; + } + + private void drain() { + if (wip.getAndIncrement() == 0) { + final Subscriber c = child; + final Queue q = queue; + + do { + if (checkTerminated(done, q.isEmpty())) { // (1) + return; + } + + wip.lazySet(1); + + long r = get(); + long e = 0; + + while (r != 0) { + boolean d = done; + Object v = q.poll(); + if (checkTerminated(d, v == null)) { + return; + } else + if (v == null) { + break; + } + + try { + if (v == NULL_SENTINEL) { + c.onNext(null); + } else { + @SuppressWarnings("unchecked") + T t = (T)v; + c.onNext(t); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + Throwable ex1 = OnErrorThrowable.addValueAsLastCause(ex, v != NULL_SENTINEL ? v : null); + c.onError(ex1); + return; + } + r--; + e++; + } + + if (e != 0 && get() != Long.MAX_VALUE) { + addAndGet(-e); + } + } while (wip.decrementAndGet() != 0); + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/producers/QueuedValueProducer.java b/src/main/java/rx/internal/producers/QueuedValueProducer.java new file mode 100644 index 0000000000..df61a05041 --- /dev/null +++ b/src/main/java/rx/internal/producers/QueuedValueProducer.java @@ -0,0 +1,138 @@ +/** + * Copyright 2015 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.producers; + +import java.util.Queue; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.exceptions.*; +import rx.internal.operators.BackpressureUtils; +import rx.internal.util.atomic.SpscLinkedAtomicQueue; +import rx.internal.util.unsafe.*; + +/** + * Producer that holds an unbounded (or custom) queue to enqueue values and relays them + * to a child subscriber on request. + * + * @param the value type + */ +public final class QueuedValueProducer extends AtomicLong implements Producer { + + /** */ + private static final long serialVersionUID = 7277121710709137047L; + + final Subscriber child; + final Queue queue; + final AtomicInteger wip; + + static final Object NULL_SENTINEL = new Object(); + + /** + * Constructs an instance with the target child subscriber and an Spsc Linked (Atomic) Queue + * as the queue implementation. + * @param child the target child subscriber + */ + public QueuedValueProducer(Subscriber child) { + this(child, UnsafeAccess.isUnsafeAvailable() + ? new SpscLinkedQueue() : new SpscLinkedAtomicQueue()); + } + /** + * Constructs an instance with the target child subscriber and a custom queue implementation + * @param child the target child subscriber + * @param queue the queue to use + */ + public QueuedValueProducer(Subscriber child, Queue queue) { + this.child = child; + this.queue = queue; + this.wip = new AtomicInteger(); + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + if (n > 0) { + BackpressureUtils.getAndAddRequest(this, n); + drain(); + } + } + + /** + * Offers a value to this producer and tries to emit any queud 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 + */ + public boolean offer(T value) { + if (value == null) { + if (!queue.offer(NULL_SENTINEL)) { + return false; + } + } else { + if (!queue.offer(value)) { + return false; + } + } + drain(); + return true; + } + + private void drain() { + if (wip.getAndIncrement() == 0) { + final Subscriber c = child; + final Queue q = queue; + do { + if (c.isUnsubscribed()) { + return; + } + + wip.lazySet(1); + + long r = get(); + long e = 0; + Object v; + + while (r != 0 && (v = q.poll()) != null) { + try { + if (v == NULL_SENTINEL) { + c.onNext(null); + } else { + @SuppressWarnings("unchecked") + T t = (T)v; + c.onNext(t); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + Throwable ex1 = OnErrorThrowable.addValueAsLastCause(ex, v != NULL_SENTINEL ? v : null); + c.onError(ex1); + return; + } + if (c.isUnsubscribed()) { + return; + } + r--; + e++; + } + + if (e != 0 && get() != Long.MAX_VALUE) { + addAndGet(-e); + } + } while (wip.decrementAndGet() != 0); + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/producers/SingleDelayedProducer.java b/src/main/java/rx/internal/producers/SingleDelayedProducer.java new file mode 100644 index 0000000000..5da11dd80f --- /dev/null +++ b/src/main/java/rx/internal/producers/SingleDelayedProducer.java @@ -0,0 +1,115 @@ +/** + * Copyright 2015 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.producers; + +import java.util.concurrent.atomic.AtomicInteger; + +import rx.*; +import rx.exceptions.*; + +/** + * Producer that emits a single value and completes the child subscriber once that + * single value is set on it and the child requested items (maybe both asynchronously). + * + * @param the value type + */ +public final class SingleDelayedProducer extends AtomicInteger implements Producer { + /** */ + private static final long serialVersionUID = -2873467947112093874L; + /** The child to emit the value and completion once possible. */ + final Subscriber child; + /** The value to emit.*/ + T value; + + static final int NO_REQUEST_NO_VALUE = 0; + static final int NO_REQUEST_HAS_VALUE = 1; + static final int HAS_REQUEST_NO_VALUE = 2; + static final int HAS_REQUEST_HAS_VALUE = 3; + + /** + * Constructor, wraps the target child subscriber. + * @param child the child subscriber, not null + */ + public SingleDelayedProducer(Subscriber child) { + this.child = child; + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + if (n == 0) { + return; + } + for (;;) { + int s = get(); + if (s == NO_REQUEST_NO_VALUE) { + if (!compareAndSet(NO_REQUEST_NO_VALUE, HAS_REQUEST_NO_VALUE)) { + continue; + } + } else + if (s == NO_REQUEST_HAS_VALUE) { + if (compareAndSet(NO_REQUEST_HAS_VALUE, HAS_REQUEST_HAS_VALUE)) { + emit(child, value); + } + } + return; + } + } + + public void setValue(T value) { + for (;;) { + int s = get(); + if (s == NO_REQUEST_NO_VALUE) { + this.value = value; + if (!compareAndSet(NO_REQUEST_NO_VALUE, NO_REQUEST_HAS_VALUE)) { + continue; + } + } else + if (s == HAS_REQUEST_NO_VALUE) { + if (compareAndSet(HAS_REQUEST_NO_VALUE, HAS_REQUEST_HAS_VALUE)) { + emit(child, value); + } + } + return; + } + } + /** + * Emits the given value to the child subscriber and completes it + * and checks for unsubscriptions eagerly. + * @param c + * @param v + */ + private static void emit(Subscriber c, T v) { + if (c.isUnsubscribed()) { + return; + } + try { + c.onNext(v); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + Throwable e1 = OnErrorThrowable.addValueAsLastCause(e, v); + c.onError(e1); + return; + } + if (c.isUnsubscribed()) { + return; + } + c.onCompleted(); + + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/producers/SingleProducer.java b/src/main/java/rx/internal/producers/SingleProducer.java new file mode 100644 index 0000000000..8e8e17dcb4 --- /dev/null +++ b/src/main/java/rx/internal/producers/SingleProducer.java @@ -0,0 +1,79 @@ +/** + * Copyright 2015 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.producers; + +import java.util.concurrent.atomic.AtomicBoolean; + +import rx.*; +import rx.exceptions.*; + +/** + * A producer which emits a single value and completes the child on the first positive request. + * + * @param the value type + */ +public final class SingleProducer extends AtomicBoolean implements Producer { + /** */ + private static final long serialVersionUID = -3353584923995471404L; + /** The child subscriber. */ + final Subscriber child; + /** The value to be emitted. */ + final T value; + /** + * Constructs the producer with the given target child and value to be emitted. + * @param child the child subscriber, non-null + * @param value the value to be emitted, may be null + */ + public SingleProducer(Subscriber child, T value) { + this.child = child; + this.value = value; + } + @Override + public void request(long n) { + // negative requests are bugs + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + // we ignore zero requests + if (n == 0) { + return; + } + // atomically change the state into emitting mode + if (compareAndSet(false, true)) { + // avoid re-reading the instance fields + final Subscriber c = child; + T v = value; + // eagerly check for unsubscription + if (c.isUnsubscribed()) { + return; + } + // emit the value + try { + c.onNext(v); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + c.onError(OnErrorThrowable.addValueAsLastCause(e, v)); + return; + } + // eagerly check for unsubscription + if (c.isUnsubscribed()) { + return; + } + // complete the child + c.onCompleted(); + } + } +} diff --git a/src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java new file mode 100644 index 0000000000..fc8acd4aa8 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java @@ -0,0 +1,90 @@ +/* + * 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.atomic; + +import java.util.AbstractQueue; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicReference; + +abstract class BaseLinkedAtomicQueue extends AbstractQueue { + private final AtomicReference> producerNode; + private final AtomicReference> consumerNode; + public BaseLinkedAtomicQueue() { + producerNode = new AtomicReference>(); + consumerNode = new AtomicReference>(); + } + protected final LinkedQueueNode lvProducerNode() { + return producerNode.get(); + } + protected final LinkedQueueNode lpProducerNode() { + return producerNode.get(); + } + protected final void spProducerNode(LinkedQueueNode node) { + producerNode.lazySet(node); + } + protected final LinkedQueueNode xchgProducerNode(LinkedQueueNode node) { + return producerNode.getAndSet(node); + } + protected final LinkedQueueNode lvConsumerNode() { + return consumerNode.get(); + } + + protected final LinkedQueueNode lpConsumerNode() { + return consumerNode.get(); + } + protected final void spConsumerNode(LinkedQueueNode node) { + consumerNode.lazySet(node); + } + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * This is an O(n) operation as we run through all the nodes and count them.
+ * + * @see java.util.Queue#size() + */ + @Override + public final int size() { + LinkedQueueNode chaserNode = lvConsumerNode(); + final LinkedQueueNode producerNode = lvProducerNode(); + int size = 0; + // must chase the nodes all the way to the producer node, but there's no need to chase a moving target. + while (chaserNode != producerNode && size < Integer.MAX_VALUE) { + LinkedQueueNode next; + while((next = chaserNode.lvNext()) == null); + chaserNode = next; + size++; + } + return size; + } + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * Queue is empty when producerNode is the same as consumerNode. An alternative implementation would be to observe + * the producerNode.value is null, which also means an empty queue because only the consumerNode.value is allowed to + * be null. + * + * @see MessagePassingQueue#isEmpty() + */ + @Override + public final boolean isEmpty() { + return lvConsumerNode() == lvProducerNode(); + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/atomic/LinkedQueueNode.java b/src/main/java/rx/internal/util/atomic/LinkedQueueNode.java new file mode 100644 index 0000000000..9d402e4b04 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/LinkedQueueNode.java @@ -0,0 +1,54 @@ +/* + * 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.atomic; + +import java.util.concurrent.atomic.AtomicReference; + +public final class LinkedQueueNode extends AtomicReference> { + /** */ + private static final long serialVersionUID = 2404266111789071508L; + private E value; + + public LinkedQueueNode() { + } + public LinkedQueueNode(E val) { + spValue(val); + } + /** + * Gets the current value and nulls out the reference to it from this node. + * + * @return value + */ + public E getAndNullValue() { + E temp = lpValue(); + spValue(null); + return temp; + } + + public E lpValue() { + return value; + } + + public void spValue(E newValue) { + value = newValue; + } + + public void soNext(LinkedQueueNode n) { + lazySet(n); + } + + public LinkedQueueNode lvNext() { + return get(); + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java new file mode 100644 index 0000000000..51744a818a --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java @@ -0,0 +1,123 @@ +/* + * 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.atomic; + +/** + * This is a direct Java port of the MPSC algorithm as presented on 1024 + * Cores by D. Vyukov. The original has been adapted to Java and it's quirks with regards to memory model and + * layout: + *

    + *
  1. Use XCHG functionality provided by AtomicReference (which is better in JDK 8+). + *
+ * The queue is initialized with a stub node which is set to both the producer and consumer node references. From this + * point follow the notes on offer/poll. + * + * @author nitsanw + * + * @param + */ +public final class MpscLinkedAtomicQueue extends BaseLinkedAtomicQueue { + + public MpscLinkedAtomicQueue() { + super(); + LinkedQueueNode node = new LinkedQueueNode(); + spConsumerNode(node); + xchgProducerNode(node);// this ensures correct construction: StoreLoad + } + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * Offer is allowed from multiple threads.
+ * Offer allocates a new node and: + *

    + *
  1. Swaps it atomically with current producer node (only one producer 'wins') + *
  2. Sets the new node as the node following from the swapped producer node + *
+ * This works because each producer is guaranteed to 'plant' a new node and link the old node. No 2 producers can + * get the same producer node as part of XCHG guarantee. + * + * @see MessagePassingQueue#offer(Object) + * @see java.util.Queue#offer(java.lang.Object) + */ + @Override + public final boolean offer(final E nextValue) { + if (nextValue == null) { + throw new IllegalArgumentException("null elements not allowed"); + } + final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); + final LinkedQueueNode prevProducerNode = xchgProducerNode(nextNode); + // Should a producer thread get interrupted here the chain WILL be broken until that thread is resumed + // and completes the store in prev.next. + prevProducerNode.soNext(nextNode); // StoreStore + return true; + } + + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * Poll is allowed from a SINGLE thread.
+ * Poll reads the next node from the consumerNode and: + *

    + *
  1. If it is null, the queue is assumed empty (though it might not be). + *
  2. If it is not null set it as the consumer node and return it's now evacuated value. + *
+ * This means the consumerNode.value is always null, which is also the starting point for the queue. Because null + * values are not allowed to be offered this is the only node with it's value set to null at any one time. + * + * @see MessagePassingQueue#poll() + * @see java.util.Queue#poll() + */ + @Override + public final E poll() { + LinkedQueueNode currConsumerNode = lpConsumerNode(); // don't load twice, it's alright + LinkedQueueNode nextNode = currConsumerNode.lvNext(); + if (nextNode != null) { + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + spConsumerNode(nextNode); + return nextValue; + } + else if (currConsumerNode != lvProducerNode()) { + // spin, we are no longer wait free + while((nextNode = currConsumerNode.lvNext()) == null); + // got the next node... + + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + spConsumerNode(nextNode); + return nextValue; + } + return null; + } + + @Override + public final E peek() { + LinkedQueueNode currConsumerNode = lpConsumerNode(); // don't load twice, it's alright + LinkedQueueNode nextNode = currConsumerNode.lvNext(); + if (nextNode != null) { + return nextNode.lpValue(); + } + else if (currConsumerNode != lvProducerNode()) { + // spin, we are no longer wait free + while((nextNode = currConsumerNode.lvNext()) == null); + // got the next node... + return nextNode.lpValue(); + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java new file mode 100644 index 0000000000..b7778ec18e --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java @@ -0,0 +1,103 @@ +/* + * 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.atomic; + +/** + * This is a weakened version of the MPSC algorithm as presented on 1024 + * Cores by D. Vyukov. The original has been adapted to Java and it's quirks with regards to memory model and + * layout: + *
    + *
  1. As this is an SPSC we have no need for XCHG, an ordered store is enough. + *
+ * The queue is initialized with a stub node which is set to both the producer and consumer node references. From this + * point follow the notes on offer/poll. + * + * @author nitsanw + * + * @param + */ +public final class SpscLinkedAtomicQueue extends BaseLinkedAtomicQueue { + + public SpscLinkedAtomicQueue() { + super(); + LinkedQueueNode node = new LinkedQueueNode(); + spProducerNode(node); + spConsumerNode(node); + node.soNext(null); // this ensures correct construction: StoreStore + } + + /** + * {@inheritDoc}
+ * + * IMPLEMENTATION NOTES:
+ * Offer is allowed from a SINGLE thread.
+ * Offer allocates a new node (holding the offered value) and: + *
    + *
  1. Sets that node as the producerNode.next + *
  2. Sets the new node as the producerNode + *
+ * From this follows that producerNode.next is always null and for all other nodes node.next is not null. + * + * @see MessagePassingQueue#offer(Object) + * @see java.util.Queue#offer(java.lang.Object) + */ + @Override + public boolean offer(final E nextValue) { + if (nextValue == null) { + throw new IllegalArgumentException("null elements not allowed"); + } + final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); + lpProducerNode().soNext(nextNode); + spProducerNode(nextNode); + return true; + } + + /** + * {@inheritDoc}
+ * + * IMPLEMENTATION NOTES:
+ * Poll is allowed from a SINGLE thread.
+ * Poll reads the next node from the consumerNode and: + *
    + *
  1. If it is null, the queue is empty. + *
  2. If it is not null set it as the consumer node and return it's now evacuated value. + *
+ * This means the consumerNode.value is always null, which is also the starting point for the queue. Because null + * values are not allowed to be offered this is the only node with it's value set to null at any one time. + * + */ + @Override + public E poll() { + final LinkedQueueNode nextNode = lpConsumerNode().lvNext(); + if (nextNode != null) { + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + spConsumerNode(nextNode); + return nextValue; + } + return null; + } + + @Override + public E peek() { + final LinkedQueueNode nextNode = lpConsumerNode().lvNext(); + if (nextNode != null) { + return nextNode.lpValue(); + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java new file mode 100644 index 0000000000..4f0106a56f --- /dev/null +++ b/src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java @@ -0,0 +1,123 @@ +/* + * 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.unsafe; + +import static rx.internal.util.unsafe.UnsafeAccess.UNSAFE; + +import java.util.*; + +import rx.internal.util.atomic.LinkedQueueNode; + +abstract class BaseLinkedQueuePad0 extends AbstractQueue { + long p00, p01, p02, p03, p04, p05, p06, p07; + long p30, p31, p32, p33, p34, p35, p36, p37; +} + +abstract class BaseLinkedQueueProducerNodeRef extends BaseLinkedQueuePad0 { + protected final static long P_NODE_OFFSET = UnsafeAccess.addressOf(BaseLinkedQueueProducerNodeRef.class, "producerNode"); + + protected LinkedQueueNode producerNode; + protected final void spProducerNode(LinkedQueueNode node) { + producerNode = node; + } + + @SuppressWarnings("unchecked") + protected final LinkedQueueNode lvProducerNode() { + return (LinkedQueueNode) UNSAFE.getObjectVolatile(this, P_NODE_OFFSET); + } + + protected final LinkedQueueNode lpProducerNode() { + return producerNode; + } +} + +abstract class BaseLinkedQueuePad1 extends BaseLinkedQueueProducerNodeRef { + long p00, p01, p02, p03, p04, p05, p06, p07; + long p30, p31, p32, p33, p34, p35, p36, p37; +} + +abstract class BaseLinkedQueueConsumerNodeRef extends BaseLinkedQueuePad1 { + protected final static long C_NODE_OFFSET = UnsafeAccess.addressOf(BaseLinkedQueueConsumerNodeRef.class, "consumerNode"); + protected LinkedQueueNode consumerNode; + protected final void spConsumerNode(LinkedQueueNode node) { + consumerNode = node; + } + + @SuppressWarnings("unchecked") + protected final LinkedQueueNode lvConsumerNode() { + return (LinkedQueueNode) UNSAFE.getObjectVolatile(this, C_NODE_OFFSET); + } + + protected final LinkedQueueNode lpConsumerNode() { + return consumerNode; + } +} + +/** + * A base data structure for concurrent linked queues. + * + * @author nitsanw + * + * @param + */ +abstract class BaseLinkedQueue extends BaseLinkedQueueConsumerNodeRef { + long p00, p01, p02, p03, p04, p05, p06, p07; + long p30, p31, p32, p33, p34, p35, p36, p37; + + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * This is an O(n) operation as we run through all the nodes and count them.
+ * + * @see java.util.Queue#size() + */ + @Override + public final int size() { + // Read consumer first, this is important because if the producer is node is 'older' than the consumer the + // consumer may overtake it (consume past it). This will lead to an infinite loop below. + LinkedQueueNode chaserNode = lvConsumerNode(); + final LinkedQueueNode producerNode = lvProducerNode(); + int size = 0; + // must chase the nodes all the way to the producer node, but there's no need to chase a moving target. + while (chaserNode != producerNode && size < Integer.MAX_VALUE) { + LinkedQueueNode next; + while((next = chaserNode.lvNext()) == null); + chaserNode = next; + size++; + } + return size; + } + + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * Queue is empty when producerNode is the same as consumerNode. An alternative implementation would be to observe + * the producerNode.value is null, which also means an empty queue because only the consumerNode.value is allowed to + * be null. + * + * @see MessagePassingQueue#isEmpty() + */ + @Override + public final boolean isEmpty() { + return lvConsumerNode() == lvProducerNode(); + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java new file mode 100644 index 0000000000..0252db5ed3 --- /dev/null +++ b/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java @@ -0,0 +1,133 @@ +/* + * 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.unsafe; + +import static rx.internal.util.unsafe.UnsafeAccess.UNSAFE; +import rx.internal.util.atomic.LinkedQueueNode; +/** + * This is a direct Java port of the MPSC algorithm as presented on 1024 + * Cores by D. Vyukov. The original has been adapted to Java and it's quirks with regards to memory model and + * layout: + *

    + *
  1. Use inheritance to ensure no false sharing occurs between producer/consumer node reference fields. + *
  2. Use XCHG functionality to the best of the JDK ability (see differences in JDK7/8 impls). + *
+ * The queue is initialized with a stub node which is set to both the producer and consumer node references. From this + * point follow the notes on offer/poll. + * + * @author nitsanw + * + * @param + */ +public final class MpscLinkedQueue extends BaseLinkedQueue { + + public MpscLinkedQueue() { + consumerNode = new LinkedQueueNode(); + xchgProducerNode(consumerNode);// this ensures correct construction: StoreLoad + } + + @SuppressWarnings("unchecked") + protected final LinkedQueueNode xchgProducerNode(LinkedQueueNode newVal) { + Object oldVal; + do { + oldVal = producerNode; + } while(!UNSAFE.compareAndSwapObject(this, P_NODE_OFFSET, oldVal, newVal)); + return (LinkedQueueNode) oldVal; + } + + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * Offer is allowed from multiple threads.
+ * Offer allocates a new node and: + *

    + *
  1. Swaps it atomically with current producer node (only one producer 'wins') + *
  2. Sets the new node as the node following from the swapped producer node + *
+ * This works because each producer is guaranteed to 'plant' a new node and link the old node. No 2 producers can + * get the same producer node as part of XCHG guarantee. + * + * @see MessagePassingQueue#offer(Object) + * @see java.util.Queue#offer(java.lang.Object) + */ + @Override + public final boolean offer(final E nextValue) { + if (nextValue == null) { + throw new IllegalArgumentException("null elements not allowed"); + } + final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); + final LinkedQueueNode prevProducerNode = xchgProducerNode(nextNode); + // Should a producer thread get interrupted here the chain WILL be broken until that thread is resumed + // and completes the store in prev.next. + prevProducerNode.soNext(nextNode); // StoreStore + return true; + } + + /** + * {@inheritDoc}
+ *

+ * IMPLEMENTATION NOTES:
+ * Poll is allowed from a SINGLE thread.
+ * Poll reads the next node from the consumerNode and: + *

    + *
  1. If it is null, the queue is assumed empty (though it might not be). + *
  2. If it is not null set it as the consumer node and return it's now evacuated value. + *
+ * This means the consumerNode.value is always null, which is also the starting point for the queue. Because null + * values are not allowed to be offered this is the only node with it's value set to null at any one time. + * + * @see MessagePassingQueue#poll() + * @see java.util.Queue#poll() + */ + @Override + public final E poll() { + LinkedQueueNode currConsumerNode = lpConsumerNode(); // don't load twice, it's alright + LinkedQueueNode nextNode = currConsumerNode.lvNext(); + if (nextNode != null) { + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + spConsumerNode(nextNode); + return nextValue; + } + else if (currConsumerNode != lvProducerNode()) { + // spin, we are no longer wait free + while((nextNode = currConsumerNode.lvNext()) == null); + // got the next node... + + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + consumerNode = nextNode; + return nextValue; + } + return null; + } + + @Override + public final E peek() { + LinkedQueueNode currConsumerNode = consumerNode; // don't load twice, it's alright + LinkedQueueNode nextNode = currConsumerNode.lvNext(); + if (nextNode != null) { + return nextNode.lpValue(); + } + else if (currConsumerNode != lvProducerNode()) { + // spin, we are no longer wait free + while((nextNode = currConsumerNode.lvNext()) == null); + // got the next node... + return nextNode.lpValue(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java new file mode 100644 index 0000000000..74ccc1b50b --- /dev/null +++ b/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java @@ -0,0 +1,105 @@ +/* + * 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.unsafe; + +import rx.internal.util.atomic.LinkedQueueNode; + + + +/** + * This is a weakened version of the MPSC algorithm as presented on 1024 + * Cores by D. Vyukov. The original has been adapted to Java and it's quirks with regards to memory model and + * layout: + *
    + *
  1. Use inheritance to ensure no false sharing occurs between producer/consumer node reference fields. + *
  2. As this is an SPSC we have no need for XCHG, an ordered store is enough. + *
+ * The queue is initialized with a stub node which is set to both the producer and consumer node references. From this + * point follow the notes on offer/poll. + * + * @author nitsanw + * + * @param + */ +public final class SpscLinkedQueue extends BaseLinkedQueue { + + public SpscLinkedQueue() { + spProducerNode(new LinkedQueueNode()); + spConsumerNode(producerNode); + consumerNode.soNext(null); // this ensures correct construction: StoreStore + } + + /** + * {@inheritDoc}
+ * + * IMPLEMENTATION NOTES:
+ * Offer is allowed from a SINGLE thread.
+ * Offer allocates a new node (holding the offered value) and: + *
    + *
  1. Sets that node as the producerNode.next + *
  2. Sets the new node as the producerNode + *
+ * From this follows that producerNode.next is always null and for all other nodes node.next is not null. + * + * @see MessagePassingQueue#offer(Object) + * @see java.util.Queue#offer(java.lang.Object) + */ + @Override + public boolean offer(final E nextValue) { + if (nextValue == null) { + throw new IllegalArgumentException("null elements not allowed"); + } + final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); + producerNode.soNext(nextNode); + producerNode = nextNode; + return true; + } + + /** + * {@inheritDoc}
+ * + * IMPLEMENTATION NOTES:
+ * Poll is allowed from a SINGLE thread.
+ * Poll reads the next node from the consumerNode and: + *
    + *
  1. If it is null, the queue is empty. + *
  2. If it is not null set it as the consumer node and return it's now evacuated value. + *
+ * This means the consumerNode.value is always null, which is also the starting point for the queue. Because null + * values are not allowed to be offered this is the only node with it's value set to null at any one time. + * + */ + @Override + public E poll() { + final LinkedQueueNode nextNode = consumerNode.lvNext(); + if (nextNode != null) { + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + consumerNode = nextNode; + return nextValue; + } + return null; + } + + @Override + public E peek() { + final LinkedQueueNode nextNode = consumerNode.lvNext(); + if (nextNode != null) { + return nextNode.lpValue(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java b/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java index a5cbcb5d40..88d0ebf4dd 100644 --- a/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java +++ b/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java @@ -84,4 +84,25 @@ public static int getAndSetInt(Object obj, long offset, int newValue) { public static boolean compareAndSwapInt(Object obj, long offset, int expected, int newValue) { return UNSAFE.compareAndSwapInt(obj, offset, expected, newValue); } + + /** + * Returns the address of the specific field on the class and + * wraps a NoSuchFieldException into an internal error. + *

+ * One can avoid using static initializers this way and just assign + * the address directly to the target static field. + * @param clazz the target class + * @param fieldName the target field name + * @return the address (offset) of the field + */ + public static long addressOf(Class clazz, String fieldName) { + try { + Field f = clazz.getDeclaredField(fieldName); + return UNSAFE.objectFieldOffset(f); + } catch (NoSuchFieldException ex) { + InternalError ie = new InternalError(); + ie.initCause(ex); + throw ie; + } + } } \ No newline at end of file diff --git a/src/test/java/rx/internal/producers/ProducersTest.java b/src/test/java/rx/internal/producers/ProducersTest.java new file mode 100644 index 0000000000..ee746335fd --- /dev/null +++ b/src/test/java/rx/internal/producers/ProducersTest.java @@ -0,0 +1,381 @@ +/** + * Copyright 2015 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.producers; + +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.*; + +import rx.*; +import rx.Observable.OnSubscribe; +import rx.Observable; +import rx.Observer; +import rx.functions.*; +import rx.observers.TestSubscriber; +import rx.schedulers.*; +import rx.subscriptions.SerialSubscription; + +public class ProducersTest { + @Test + public void testSingleNoBackpressure() { + TestSubscriber ts = new TestSubscriber(); + SingleProducer sp = new SingleProducer(ts, 1); + ts.setProducer(sp); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1)); + } + @Test + public void testSingleWithBackpressure() { + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(0); + SingleProducer sp = new SingleProducer(ts, 1); + ts.setProducer(sp); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.emptyList()); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + + ts.requestMore(2); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1)); + } + + @Test + public void testSingleDelayedNoBackpressure() { + TestSubscriber ts = new TestSubscriber(); + SingleDelayedProducer sdp = new SingleDelayedProducer(ts); + ts.setProducer(sdp); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.emptyList()); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + + sdp.setValue(1); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1)); + + sdp.setValue(2); + + ts.assertReceivedOnNext(Arrays.asList(1)); + } + @Test + public void testSingleDelayedWithBackpressure() { + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(0); + SingleDelayedProducer sdp = new SingleDelayedProducer(ts); + ts.setProducer(sdp); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.emptyList()); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + + sdp.setValue(1); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.emptyList()); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + + ts.requestMore(2); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1)); + + sdp.setValue(2); + + ts.assertReceivedOnNext(Arrays.asList(1)); + } + + @Test + public void testQueuedValueNoBackpressure() { + TestSubscriber ts = new TestSubscriber(); + QueuedValueProducer qvp = new QueuedValueProducer(ts); + ts.setProducer(qvp); + + qvp.offer(1); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1)); + + qvp.offer(2); + qvp.offer(3); + qvp.offer(4); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4)); + } + @Test + public void testQueuedValueWithBackpressure() { + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(0); + QueuedValueProducer qvp = new QueuedValueProducer(ts); + ts.setProducer(qvp); + + qvp.offer(1); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.emptyList()); + + qvp.offer(2); + ts.requestMore(2); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2)); + + ts.requestMore(2); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2)); + + qvp.offer(3); + qvp.offer(4); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4)); + } + + @Test + public void testQueuedNoBackpressure() { + TestSubscriber ts = new TestSubscriber(); + QueuedProducer qp = new QueuedProducer(ts); + ts.setProducer(qp); + + qp.offer(1); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1)); + + qp.offer(2); + qp.offer(3); + qp.offer(4); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4)); + + qp.onCompleted(); + + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4)); + } + @Test + public void testQueuedWithBackpressure() { + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(0); + QueuedProducer qp = new QueuedProducer(ts); + ts.setProducer(qp); + + qp.offer(1); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Collections.emptyList()); + + qp.offer(2); + ts.requestMore(2); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2)); + + ts.requestMore(2); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2)); + + qp.offer(3); + qp.offer(4); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4)); + + qp.onCompleted(); + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4)); + } + + @Test + public void testArbiter() { + Producer p1 = mock(Producer.class); + Producer p2 = mock(Producer.class); + + ProducerArbiter pa = new ProducerArbiter(); + + pa.request(100); + + pa.setProducer(p1); + + verify(p1).request(100); + + pa.produced(50); + + pa.setProducer(p2); + + verify(p2).request(50); + } + + static final class TestProducer implements Producer { + final Observer child; + public TestProducer(Observer child) { + this.child = child; + } + @Override + public void request(long n) { + child.onNext((int)n); + } + } + + @Test + public void testObserverArbiterWithBackpressure() { + final TestSubscriber ts = new TestSubscriber(); + ts.requestMore(0); + final ProducerObserverArbiter poa = new ProducerObserverArbiter(ts); + ts.setProducer(poa); + + + poa.setProducer(new TestProducer(poa)); + + ts.requestMore(1); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1)); + + poa.setProducer(null); + ts.requestMore(5); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1)); + + poa.setProducer(new TestProducer(poa)); + + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(1, 5)); + + poa.onCompleted(); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1, 5)); + } + static final class SwitchTimer + implements OnSubscribe { + final List> sources; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + public SwitchTimer( + Iterable> sources, + long time, TimeUnit unit, Scheduler scheduler) { + this.scheduler = scheduler; + this.sources = new ArrayList>(); + this.time = time; + this.unit = unit; + for (Observable o : sources) { + this.sources.add(o); + } + } + @Override + public void call(Subscriber child) { + final ProducerObserverArbiter poa = + new ProducerObserverArbiter(child); + + Scheduler.Worker w = scheduler.createWorker(); + child.add(w); + + child.setProducer(poa); + + final SerialSubscription ssub = new SerialSubscription(); + child.add(ssub); + + final int[] index = new int[1]; + + w.schedulePeriodically(new Action0() { + @Override + public void call() { + final int idx = index[0]++; + if (idx >= sources.size()) { + poa.onCompleted(); + return; + } + Subscriber s = new Subscriber() { + @Override + public void onNext(T t) { + poa.onNext(t); + } + @Override + public void onError(Throwable e) { + poa.onError(e); + } + @Override + public void onCompleted() { + if (idx + 1 == sources.size()) { + poa.onCompleted(); + } + } + @Override + public void setProducer(Producer producer) { + poa.setProducer(producer); + } + }; + + ssub.set(s); + sources.get(idx).unsafeSubscribe(s); + } + }, time, time, unit); + } + } + final Func1 plus(final long n) { + return new Func1() { + @Override + public Long call(Long t) { + return t + n; + } + }; + } + @Test + public void testObserverArbiterAsync() { + TestScheduler test = Schedulers.test(); + @SuppressWarnings("unchecked") + List> timers = Arrays.asList( + Observable.timer(100, 100, TimeUnit.MILLISECONDS, test), + Observable.timer(100, 100, TimeUnit.MILLISECONDS, test) + .map(plus(20)), + Observable.timer(100, 100, TimeUnit.MILLISECONDS, test) + .map(plus(40)) + ); + + Observable source = Observable.create( + new SwitchTimer(timers, 550, + TimeUnit.MILLISECONDS, test)); + + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(100); + source.subscribe(ts); + + test.advanceTimeBy(1, TimeUnit.MINUTES); + + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertReceivedOnNext(Arrays.asList(0L, 1L, 2L, 3L, 4L, + 20L, 21L, 22L, 23L, 24L, + 40L, 41L, 42L, 43L, 44L)); + } +} From ad0d4225af00661887a5417dc2b27eebc461fcf4 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 19 May 2015 18:36:05 +0200 Subject: [PATCH 270/857] Added links to the source of the new queue classes. --- .../java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java | 3 +++ src/main/java/rx/internal/util/atomic/LinkedQueueNode.java | 3 +++ .../java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java | 3 +++ .../java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java | 3 +++ src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java | 3 +++ src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java | 3 +++ src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java | 3 +++ 7 files changed, 21 insertions(+) diff --git a/src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java index fc8acd4aa8..f46890b7f1 100644 --- a/src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java +++ b/src/main/java/rx/internal/util/atomic/BaseLinkedAtomicQueue.java @@ -10,6 +10,9 @@ * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/BaseLinkedAtomicQueue.java */ package rx.internal.util.atomic; diff --git a/src/main/java/rx/internal/util/atomic/LinkedQueueNode.java b/src/main/java/rx/internal/util/atomic/LinkedQueueNode.java index 9d402e4b04..d687460c64 100644 --- a/src/main/java/rx/internal/util/atomic/LinkedQueueNode.java +++ b/src/main/java/rx/internal/util/atomic/LinkedQueueNode.java @@ -10,6 +10,9 @@ * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/LinkedQueueNode.java */ package rx.internal.util.atomic; diff --git a/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java index 51744a818a..ebc1264599 100644 --- a/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java +++ b/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java @@ -10,6 +10,9 @@ * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/MpscLinkedAtomicQueue.java */ package rx.internal.util.atomic; diff --git a/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java index b7778ec18e..5832f7371d 100644 --- a/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java +++ b/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java @@ -10,6 +10,9 @@ * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/SpscLinkedAtomicQueue.java */ package rx.internal.util.atomic; diff --git a/src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java index 4f0106a56f..05bcb798fb 100644 --- a/src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java +++ b/src/main/java/rx/internal/util/unsafe/BaseLinkedQueue.java @@ -10,6 +10,9 @@ * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/BaseLinkedQueue.java */ package rx.internal.util.unsafe; diff --git a/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java index 0252db5ed3..f9e63f1c6b 100644 --- a/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java +++ b/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java @@ -10,6 +10,9 @@ * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/MpscLinkedQueue.java */ package rx.internal.util.unsafe; diff --git a/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java index 74ccc1b50b..7c3c675b48 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java @@ -10,6 +10,9 @@ * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/SpscLinkedQueue.java */ package rx.internal.util.unsafe; From 536d0197573f47975d94d9507c924017ed203d64 Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 19 May 2015 10:34:41 -0700 Subject: [PATCH 271/857] The usual anally-retentive javadoc edits. --- .../java/rx/observers/TestSubscriber.java | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 611f1e1137..56d12b46e2 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -56,7 +56,9 @@ public void onNext(Object t) { /** * Constructs a TestSubscriber with the initial request to be requested from upstream. + * * @param initialRequest the initial request value, negative value will revert to the default unbounded behavior + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @SuppressWarnings("unchecked") @Experimental @@ -67,8 +69,10 @@ public TestSubscriber(long initialRequest) { /** * Constructs a TestSubscriber with the initial request to be requested from upstream * and a delegate Observer to wrap. + * * @param initialRequest the initial request value, negative value will revert to the default unbounded behavior * @param delegate the Observer instance to wrap + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public TestSubscriber(Observer delegate, long initialRequest) { @@ -78,7 +82,7 @@ public TestSubscriber(Observer delegate, long initialRequest) { this.testObserver = new TestObserver(delegate); this.initialRequest = initialRequest; } - + public TestSubscriber(Subscriber delegate) { this(delegate, -1); } @@ -305,6 +309,9 @@ public Thread getLastSeenThread() { /** * Assert if there is exactly a single completion event. + * + * @throws AssertionError if there were zero, or more than one, onCompleted events + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertCompleted() { @@ -316,8 +323,12 @@ public void assertCompleted() { throw new AssertionError("Completed multiple times: " + s); } } + /** * Assert if there is no completion event. + * + * @throws AssertionError if there were one or more than one onCompleted events + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertNotCompleted() { @@ -329,9 +340,14 @@ public void assertNotCompleted() { throw new AssertionError("Completed multiple times: " + s); } } + /** * Assert if there is exactly one error event which is a subclass of the given class. + * * @param clazz the class to check the error against. + * @throws AssertionError if there were zero, or more than one, onError events, or if the single onError + * event did not carry an error of a subclass of the given class + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertError(Class clazz) { @@ -346,9 +362,14 @@ public void assertError(Class clazz) { throw new AssertionError("Exceptions differ; expected: " + clazz + ", actual: " + err.get(0), err.get(0)); } } + /** * Assert there is a single onError event with the exact exception. + * * @param throwable the throwable to check + * @throws AssertionError if there were zero, or more than one, onError events, or if the single onError + * event did not carry an error that matches the specified throwable + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertError(Throwable throwable) { @@ -363,8 +384,12 @@ public void assertError(Throwable throwable) { throw new AssertionError("Exceptions differ; expected: " + throwable + ", actual: " + err.get(0), err.get(0)); } } + /** * Assert for no onError and onCompleted events. + * + * @throws AssertionError if there was either an onError or onCompleted event + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertNoTerminalEvent() { @@ -381,8 +406,12 @@ public void assertNoTerminalEvent() { } } } + /** * Assert if there are no onNext events received. + * + * @throws AssertionError if there were any onNext events + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertNoValues() { @@ -391,9 +420,13 @@ public void assertNoValues() { throw new AssertionError("No onNext events expected yet some received: " + s); } } + /** * Assert if the given number of onNext events are received. + * * @param count the expected number of onNext events + * @throws AssertionError if there were more or fewer onNext events than specified by {@code count} + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertValueCount(int count) { @@ -404,19 +437,26 @@ public void assertValueCount(int count) { } /** - * Assert if the received onNext events, in order, are the specified values. - * @param values the values to check + * Assert if the received onNext events, in order, are the specified items. + * + * @param values the items to check + * @throws AssertionError if the items emitted do not exactly match those specified by {@code values} + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertValues(T... values) { assertReceivedOnNext(Arrays.asList(values)); } + /** - * Assert if there is only a single received onNext event. - * @param values the values to check + * Assert if there is only a single received onNext event and that it marks the emission of a specific item. + * + * @param value the item to check + * @throws AssertionError if the Observable does not emit only the single item specified by {@code value} + * @since (if this graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public void assertValue(T value) { assertReceivedOnNext(Collections.singletonList(value)); } -} \ No newline at end of file +} From 2061d4f065213ba2fb039f73db8a1fbe832c8ce6 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 19 May 2015 11:51:47 -0700 Subject: [PATCH 272/857] 1.0.11 --- CHANGES.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1a6920f16c..810b586189 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,46 @@ # RxJava Releases # +### Version 1.0.11 – May 19th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.11%7C)) ### + +* [Pull 2948] (https://github.com/ReactiveX/RxJava/pull/2948) More assertions for TestSubscriber +* [Pull 2956] (https://github.com/ReactiveX/RxJava/pull/2956) OperatorObserveOn should not request more after child is unsubscribed +* [Pull 2951] (https://github.com/ReactiveX/RxJava/pull/2951) OperatorConcat - prevent request overflow and fix race condition +* [Pull 2950] (https://github.com/ReactiveX/RxJava/pull/2950) OperatorGroupBy - check for request overflow and don't decrement when at Long.MAX_VALUE +* [Pull 2923] (https://github.com/ReactiveX/RxJava/pull/2923) OnBackpressureLatest: Non-blocking version of the toBlocking().latest() operator. +* [Pull 2907] (https://github.com/ReactiveX/RxJava/pull/2907) Fixed schedule race and task retention with ExecutorScheduler. +* [Pull 2929] (https://github.com/ReactiveX/RxJava/pull/2929) OperatorObserveOn onComplete can be emitted despite onError being called +* [Pull 2940] (https://github.com/ReactiveX/RxJava/pull/2940) Remove unnecessary localHasValue check +* [Pull 2939] (https://github.com/ReactiveX/RxJava/pull/2939) publish: Fix another race between terminalEvent and the queue being empty. +* [Pull 2938] (https://github.com/ReactiveX/RxJava/pull/2938) Fixed Observable.combineLatest overflow bug on Android +* [Pull 2936] (https://github.com/ReactiveX/RxJava/pull/2936) Fix TestSubject bug +* [Pull 2934] (https://github.com/ReactiveX/RxJava/pull/2934) Fix termination race condition in OperatorPublish.dispatch +* [Pull 2963] (https://github.com/ReactiveX/RxJava/pull/2963) Set of standard producers and updated queue implementations with some + +### Version 1.0.10 – April 30th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.10%7C)) ### + +* [Pull 2892] (https://github.com/ReactiveX/RxJava/pull/2892) Fix Observable.range race condition +* [Pull 2895] (https://github.com/ReactiveX/RxJava/pull/2895) Fix Observable.from(Iterable) race condition +* [Pull 2898] (https://github.com/ReactiveX/RxJava/pull/2898) Observable.range - add unit test for eager completion on empty +* [Pull 2899] (https://github.com/ReactiveX/RxJava/pull/2899) Observable.from(empty) to emit onComplete even when 0 requested +* [Pull 2894] (https://github.com/ReactiveX/RxJava/pull/2894) Concat: fixed reentrancy problem in completeInner +* [Pull 2880] (https://github.com/ReactiveX/RxJava/pull/2880) Use singleton reduction functions in count and countLong +* [Pull 2902] (https://github.com/ReactiveX/RxJava/pull/2902) Prevent ExceptionsTest from hanging when testing stack overflow +* [Pull 2897] (https://github.com/ReactiveX/RxJava/pull/2897) Fix for overlapping windows. +* [Pull 2904] (https://github.com/ReactiveX/RxJava/pull/2904) TakeLast - add request overflow check +* [Pull 2905] (https://github.com/ReactiveX/RxJava/pull/2905) Use singleton Operators where we can +* [Pull 2909] (https://github.com/ReactiveX/RxJava/pull/2909) Fix the drainer to check if the queue is empty before quitting. +* [Pull 2911] (https://github.com/ReactiveX/RxJava/pull/2911) OperatorPublish benchmark +* [Pull 2914] (https://github.com/ReactiveX/RxJava/pull/2914) Optimization - use OperatorTakeLastOne for takeLast(1) +* [Pull 2915] (https://github.com/ReactiveX/RxJava/pull/2915) Observable.ignoreElements - optimize +* [Pull 2883] (https://github.com/ReactiveX/RxJava/pull/2883) Proposal: standardized Subject state-peeking methods. +* [Pull 2901] (https://github.com/ReactiveX/RxJava/pull/2901) Operators toList and toSortedList now support backpressure +* [Pull 2921] (https://github.com/ReactiveX/RxJava/pull/2921) OperatorObserveOn - handle request overflow correctly +* [Pull 2882] (https://github.com/ReactiveX/RxJava/pull/2882) OperatorScan - don't call onNext after onError is called +* [Pull 2875] (https://github.com/ReactiveX/RxJava/pull/2875) Fix: NPE in requestFromChild method. +* [Pull 2814] (https://github.com/ReactiveX/RxJava/pull/2814) Operator Publish full rewrite +* [Pull 2820] (https://github.com/ReactiveX/RxJava/pull/2820) Backpressure for window(size) +* [Pull 2912] (https://github.com/ReactiveX/RxJava/pull/2912) Fix the Scheduler performance degradation + ### Version 1.0.9 – April 9th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.9%7C)) ### * [Pull 2845] (https://github.com/ReactiveX/RxJava/pull/2845) Fix for repeat: wrong target of request From b0c8b42b23d2d4ced039ad113a60c3f6db71b5ca Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 20 May 2015 10:40:51 +0200 Subject: [PATCH 273/857] Deprecated onBackpressureBlock. --- src/main/java/rx/Observable.java | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 919548fd32..9b0479cc0c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5332,14 +5332,23 @@ public final Observable onBackpressureDrop() { *

* Note that if the upstream Observable does support backpressure, this operator ignores that capability * and doesn't propagate any backpressure requests from downstream. + *

+ * Warning! Using a chain like {@code source.onBackpressureBlock().subscribeOn(scheduler)} is prone to + * deadlocks because the consumption of the internal queue is scheduled behind a blocked emission by + * the subscribeOn. In order to avoid this, the operators have to be swapped in the chain: + * {@code source.subscribeOn(scheduler).onBackpressureBlock()} and in general, no subscribeOn operator should follow + * this operator. * * @param maxQueueLength the maximum number of items the producer can emit without blocking * @return the source Observable modified to block {@code onNext} notifications on overflow * @see ReactiveX operators documentation: backpressure operators - * @Experimental The behavior of this can change at any time. + * @Experimental The behavior of this can change at any time. + * @deprecated The operator doesn't work properly with {@link #subscribeOn(Scheduler)} and is prone to + * deadlocks. It will be removed/unavailable starting from 1.1. * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental + @Deprecated public final Observable onBackpressureBlock(int maxQueueLength) { return lift(new OperatorOnBackpressureBlock(maxQueueLength)); } @@ -5355,13 +5364,22 @@ public final Observable onBackpressureBlock(int maxQueueLength) { *

* Note that if the upstream Observable does support backpressure, this operator ignores that capability * and doesn't propagate any backpressure requests from downstream. + *

+ * Warning! Using a chain like {@code source.onBackpressureBlock().subscribeOn(scheduler)} is prone to + * deadlocks because the consumption of the internal queue is scheduled behind a blocked emission by + * the subscribeOn. In order to avoid this, the operators have to be swapped in the chain: + * {@code source.subscribeOn(scheduler).onBackpressureBlock()} and in general, no subscribeOn operator should follow + * this operator. * * @return the source Observable modified to block {@code onNext} notifications on overflow * @see ReactiveX operators documentation: backpressure operators * @Experimental The behavior of this can change at any time. + * @deprecated The operator doesn't work properly with {@link #subscribeOn(Scheduler)} and is prone to + * deadlocks. It will be removed/unavailable starting from 1.1. * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental + @Deprecated public final Observable onBackpressureBlock() { return onBackpressureBlock(rx.internal.util.RxRingBuffer.SIZE); } From c7e6cf972ab801fe07a8192b44dbab7640b83fef Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 20 May 2015 13:28:45 +0200 Subject: [PATCH 274/857] Fixed window(time) to work properly with unsubscription, added backpressure support to window(size, skip). --- src/main/java/rx/Observable.java | 2 +- .../operators/OperatorWindowWithSize.java | 60 +++++++--- .../operators/OperatorWindowWithTime.java | 106 ++++++++++-------- .../operators/OperatorWindowWithSizeTest.java | 83 +++++++++++++- .../operators/OperatorWindowWithTimeTest.java | 22 ++++ 5 files changed, 209 insertions(+), 64 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 919548fd32..f9c2d98ab9 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -9017,7 +9017,7 @@ public final Observable> window(int count) { * *

*
Backpressure Support:
- *
The operator has limited backpressure support. If {@code count} == {@code skip}, the operator honors backpressure on its outer subscriber, ignores backpressure in its inner Observables + *
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.
*
Scheduler:
*
This version of {@code window} does not operate by default on a particular {@link Scheduler}.
diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java index ed22a68bd6..62763f1948 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java @@ -48,9 +48,13 @@ public OperatorWindowWithSize(int size, int skip) { @Override public Subscriber call(Subscriber> child) { if (skip == size) { - return new ExactSubscriber(child); + ExactSubscriber e = new ExactSubscriber(child); + e.init(); + return e; } - return new InexactSubscriber(child); + InexactSubscriber ie = new InexactSubscriber(child); + ie.init(); + return ie; } /** Subscriber with exact, non-overlapping window bounds. */ final class ExactSubscriber extends Subscriber { @@ -58,7 +62,6 @@ final class ExactSubscriber extends Subscriber { int count; BufferUntilSubscriber window; volatile boolean noWindow = true; - final Subscription parentSubscription = this; public ExactSubscriber(Subscriber> child) { /** * See https://github.com/ReactiveX/RxJava/issues/1546 @@ -69,13 +72,15 @@ public ExactSubscriber(Subscriber> child) { /* * Add unsubscribe hook to child to get unsubscribe on outer (unsubscribing on next window, not on the inner window itself) */ + } + 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) { - parentSubscription.unsubscribe(); + unsubscribe(); } } @@ -111,7 +116,7 @@ public void onNext(T t) { window = null; noWindow = true; if (child.isUnsubscribed()) { - parentSubscription.unsubscribe(); + unsubscribe(); return; } } @@ -139,7 +144,7 @@ final class InexactSubscriber extends Subscriber { final Subscriber> child; int count; final List> chunks = new LinkedList>(); - final Subscription parentSubscription = this; + volatile boolean noWindow = true; public InexactSubscriber(Subscriber> child) { /** @@ -148,6 +153,9 @@ public InexactSubscriber(Subscriber> child) { * applies to the outer, not the inner. */ this.child = child; + } + + void init() { /* * Add unsubscribe hook to child to get unsubscribe on outer (unsubscribing on next window, not on the inner window itself) */ @@ -156,24 +164,38 @@ public InexactSubscriber(Subscriber> child) { @Override public void call() { // if no window we unsubscribe up otherwise wait until window ends - if (chunks == null || chunks.size() == 0) { - parentSubscription.unsubscribe(); + if (noWindow) { + unsubscribe(); } } })); - } - - @Override - public void onStart() { - // no backpressure as we are controlling data flow by window size - request(Long.MAX_VALUE); + + 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); + } + } + }); } + void requestMore(long n) { + request(n); + } + @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); @@ -189,9 +211,11 @@ public void onNext(T t) { cs.consumer.onCompleted(); } } - if (chunks.size() == 0 && child.isUnsubscribed()) { - parentSubscription.unsubscribe(); - return; + if (chunks.isEmpty()) { + noWindow = true; + if (child.isUnsubscribed()) { + unsubscribe(); + } } } @@ -199,6 +223,7 @@ public void onNext(T t) { public void onError(Throwable e) { List> list = new ArrayList>(chunks); chunks.clear(); + noWindow = true; for (CountedSubject cs : list) { cs.consumer.onError(e); } @@ -209,6 +234,7 @@ public void onError(Throwable e) { public void onCompleted() { List> list = new ArrayList>(chunks); chunks.clear(); + noWindow = true; for (CountedSubject cs : list) { cs.consumer.onCompleted(); } diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithTime.java b/src/main/java/rx/internal/operators/OperatorWindowWithTime.java index dd80a06a38..cac94c5ba0 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithTime.java @@ -15,21 +15,17 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; -import rx.Observable; + +import rx.*; import rx.Observable.Operator; -import rx.Observer; -import rx.Scheduler; import rx.Scheduler.Worker; -import rx.Subscriber; +import rx.Observable; +import rx.Observer; import rx.functions.Action0; -import rx.observers.SerializedObserver; -import rx.observers.SerializedSubscriber; +import rx.observers.*; +import rx.subscriptions.Subscriptions; /** * Creates windows of values into the source sequence with timed window creation, length and size bounds. @@ -62,15 +58,16 @@ public OperatorWindowWithTime(long timespan, long timeshift, TimeUnit unit, int @Override public Subscriber call(Subscriber> child) { Worker worker = scheduler.createWorker(); - child.add(worker); if (timespan == timeshift) { ExactSubscriber s = new ExactSubscriber(child, worker); + s.add(worker); s.scheduleExact(); return s; } InexactSubscriber s = new InexactSubscriber(child, worker); + s.add(worker); s.startNewChunk(); s.scheduleChunk(); return s; @@ -118,11 +115,19 @@ final class ExactSubscriber extends Subscriber { volatile State state; public ExactSubscriber(Subscriber> child, Worker worker) { - super(child); this.child = new SerializedSubscriber>(child); this.worker = worker; this.guard = new Object(); this.state = State.empty(); + child.add(Subscriptions.create(new Action0() { + @Override + public void call() { + // if there is no active window, unsubscribe the upstream + if (state.consumer == null) { + unsubscribe(); + } + } + })); } @Override @@ -132,7 +137,6 @@ public void onStart() { @Override public void onNext(T t) { - List localQueue; synchronized (guard) { if (emitting) { if (queue == null) { @@ -141,29 +145,29 @@ public void onNext(T t) { queue.add(t); return; } - localQueue = queue; - queue = null; emitting = true; } - boolean once = true; boolean skipFinal = false; try { - do { - drain(localQueue); - if (once) { - once = false; - emitValue(t); - } + if (!emitValue(t)) { + return; + } + + for (;;) { + List localQueue; synchronized (guard) { localQueue = queue; - queue = null; if (localQueue == null) { emitting = false; skipFinal = true; return; } + queue = null; + } + if (!drain(localQueue)) { + return; } - } while (!child.isUnsubscribed()); + } } finally { if (!skipFinal) { synchronized (guard) { @@ -172,13 +176,15 @@ public void onNext(T t) { } } } - void drain(List queue) { + boolean drain(List queue) { if (queue == null) { - return; + return true; } for (Object o : queue) { if (o == NEXT_SUBJECT) { - replaceSubject(); + if (!replaceSubject()) { + return false; + } } else if (nl.isError(o)) { error(nl.getError(o)); @@ -190,23 +196,35 @@ void drain(List queue) { } else { @SuppressWarnings("unchecked") T t = (T)o; - emitValue(t); + if (!emitValue(t)) { + return false; + } } } + return true; } - void replaceSubject() { + boolean replaceSubject() { Observer s = state.consumer; if (s != null) { s.onCompleted(); } + // if child has unsubscribed, unsubscribe upstream instead of opening a new window + if (child.isUnsubscribed()) { + state = state.clear(); + unsubscribe(); + return false; + } BufferUntilSubscriber bus = BufferUntilSubscriber.create(); state = state.create(bus, bus); child.onNext(bus); + return true; } - void emitValue(T t) { + boolean emitValue(T t) { State s = state; if (s.consumer == null) { - replaceSubject(); + if (!replaceSubject()) { + return false; + } s = state; } s.consumer.onNext(t); @@ -217,6 +235,7 @@ void emitValue(T t) { s = s.next(); } state = s; + return true; } @Override @@ -285,7 +304,6 @@ public void call() { }, 0, timespan, unit); } void nextWindow() { - List localQueue; synchronized (guard) { if (emitting) { if (queue == null) { @@ -294,29 +312,29 @@ void nextWindow() { queue.add(NEXT_SUBJECT); return; } - localQueue = queue; - queue = null; emitting = true; } - boolean once = true; boolean skipFinal = false; try { - do { - drain(localQueue); - if (once) { - once = false; - replaceSubject(); - } + if (!replaceSubject()) { + return; + } + for (;;) { + List localQueue; synchronized (guard) { localQueue = queue; - queue = null; if (localQueue == null) { emitting = false; skipFinal = true; return; } + queue = null; } - } while (!child.isUnsubscribed()); + + if (!drain(localQueue)) { + return; + } + } } finally { if (!skipFinal) { synchronized (guard) { diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java index ed8333e5ec..9dade31fbc 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithSizeTest.java @@ -16,18 +16,21 @@ package rx.internal.operators; import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Test; +import org.junit.*; -import static org.mockito.Mockito.*; import rx.*; +import rx.Observable.OnSubscribe; import rx.Observable; import rx.Observer; import rx.functions.*; +import rx.internal.util.UtilityFunctions; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -245,4 +248,80 @@ public void onCompleted() { verify(o, times(1)).onCompleted(); // 1 inner } + public static Observable hotStream() { + return Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber s) { + while (!s.isUnsubscribed()) { + // burst some number of items + for (int i = 0; i < Math.random() * 20; i++) { + s.onNext(i); + } + try { + // sleep for a random amount of time + // NOTE: Only using Thread.sleep here as an artificial demo. + Thread.sleep((long) (Math.random() * 200)); + } catch (Exception e) { + // do nothing + } + } + System.out.println("Hot done."); + } + }).subscribeOn(Schedulers.newThread()); // use newThread since we are using sleep to block + } + + @Test + public void testTakeFlatMapCompletes() { + TestSubscriber ts = new TestSubscriber(); + + final int indicator = 999999999; + + hotStream() + .window(10) + .take(2) + .flatMap(new Func1, Observable>() { + @Override + public Observable call(Observable w) { + return w.startWith(indicator); + } + }).subscribe(ts); + + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts.assertCompleted(); + Assert.assertFalse(ts.getOnNextEvents().isEmpty()); + } + + @Test + @SuppressWarnings("unchecked") + public void testBackpressureOuterInexact() { + TestSubscriber> ts = new TestSubscriber>(0); + + Observable.range(1, 5).window(2, 1) + .map(new Func1, Observable>>() { + @Override + public Observable> call(Observable t) { + return t.toList(); + } + }).concatMap(UtilityFunctions.>>identity()) + .subscribe(ts); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(Arrays.asList(1, 2), Arrays.asList(2, 3)); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(5); + + System.out.println(ts.getOnNextEvents()); + + ts.assertValues(Arrays.asList(1, 2), Arrays.asList(2, 3), + Arrays.asList(3, 4), Arrays.asList(4, 5), Arrays.asList(5)); + ts.assertNoErrors(); + ts.assertCompleted(); + } } \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java index 22e6906463..34c3739c88 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithTimeTest.java @@ -26,6 +26,7 @@ import rx.Observable; import rx.Observer; import rx.functions.*; +import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; public class OperatorWindowWithTimeTest { @@ -171,4 +172,25 @@ public void testExactWindowSize() { assertEquals(Arrays.asList(10), lists.get(3)); } + @Test + public void testTakeFlatMapCompletes() { + TestSubscriber ts = new TestSubscriber(); + + final int indicator = 999999999; + + OperatorWindowWithSizeTest.hotStream() + .window(300, TimeUnit.MILLISECONDS) + .take(10) + .flatMap(new Func1, Observable>() { + @Override + public Observable call(Observable w) { + return w.startWith(indicator); + } + }).subscribe(ts); + + ts.awaitTerminalEvent(5, TimeUnit.SECONDS); + ts.assertCompleted(); + Assert.assertFalse(ts.getOnNextEvents().isEmpty()); + } + } From 7ebea13695d2be28866023fb67b48ad734ab25ee Mon Sep 17 00:00:00 2001 From: George Campbell Date: Wed, 20 May 2015 10:13:40 -0700 Subject: [PATCH 275/857] Deprecate and rename the timer methods that take initial delay and period to interval. --- src/main/java/rx/Observable.java | 70 +++++++++++++++++-- .../rx/operators/OperatorSerializePerf.java | 2 +- .../operators/OnSubscribeCacheTest.java | 2 +- .../OnSubscribeCombineLatestTest.java | 2 +- .../operators/OnSubscribeRefCountTest.java | 4 +- .../operators/OnSubscribeTimerTest.java | 4 +- .../operators/OperatorBufferTest.java | 16 ++--- .../operators/OperatorObserveOnTest.java | 4 +- .../operators/OperatorPublishTest.java | 2 +- .../rx/internal/producers/ProducersTest.java | 6 +- 10 files changed, 87 insertions(+), 25 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 919548fd32..a813373950 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1225,7 +1225,7 @@ public final static Observable from(T[] array) { * @see ReactiveX operators documentation: Interval */ public final static Observable interval(long interval, TimeUnit unit) { - return interval(interval, unit, Schedulers.computation()); + return interval(interval, interval, unit, Schedulers.computation()); } /** @@ -1248,7 +1248,65 @@ public final static Observable interval(long interval, TimeUnit unit) { * @see ReactiveX operators documentation: Interval */ public final static Observable interval(long interval, TimeUnit unit, Scheduler scheduler) { - return create(new OnSubscribeTimerPeriodically(interval, interval, unit, scheduler)); + return interval(interval, interval, unit, scheduler); + } + + /** + * Returns an Observable that emits a {@code 0L} after the {@code initialDelay} and ever increasing numbers + * after each {@code period} of time thereafter. + *

+ * + *

+ *
Backpressure Support:
+ *
This operator does not support backpressure as it uses time. If the downstream needs a slower rate + * it should slow the timer or use something like {@link #onBackpressureDrop}.
+ *
Scheduler:
+ *
{@code timer} operates by default on the {@code computation} {@link Scheduler}.
+ *
+ * + * @param initialDelay + * the initial delay time to wait before emitting the first value of 0L + * @param period + * the period of time between emissions of the subsequent numbers + * @param unit + * the time unit for both {@code initialDelay} and {@code period} + * @return an Observable that emits a 0L after the {@code initialDelay} and ever increasing numbers after + * each {@code period} of time thereafter + * @see ReactiveX operators documentation: Interval + * @since 1.0.12 + */ + public final static Observable interval(long initialDelay, long period, TimeUnit unit) { + return interval(initialDelay, period, unit, Schedulers.computation()); + } + + /** + * Returns an Observable that emits a {@code 0L} after the {@code initialDelay} and ever increasing numbers + * after each {@code period} of time thereafter, on a specified {@link Scheduler}. + *

+ * + *

+ *
Backpressure Support:
+ *
This operator does not support backpressure as it uses time. If the downstream needs a slower rate + * it should slow the timer or use something like {@link #onBackpressureDrop}.
+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param initialDelay + * the initial delay time to wait before emitting the first value of 0L + * @param period + * the period of time between emissions of the subsequent numbers + * @param unit + * the time unit for both {@code initialDelay} and {@code period} + * @param scheduler + * the Scheduler on which the waiting happens and items are emitted + * @return an Observable that emits a 0L after the {@code initialDelay} and ever increasing numbers after + * each {@code period} of time thereafter, while running on the given Scheduler + * @see ReactiveX operators documentation: Interval + * @since 1.0.12 + */ + public final static Observable interval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { + return create(new OnSubscribeTimerPeriodically(initialDelay, period, unit, scheduler)); } /** @@ -2462,9 +2520,11 @@ public final static Observable switchOnNext(ObservableReactiveX operators documentation: Timer + * @deprecated use {@link #interval(long, long, TimeUnit)} instead */ + @Deprecated public final static Observable timer(long initialDelay, long period, TimeUnit unit) { - return timer(initialDelay, period, unit, Schedulers.computation()); + return interval(initialDelay, period, unit, Schedulers.computation()); } /** @@ -2491,9 +2551,11 @@ public final static Observable timer(long initialDelay, long period, TimeU * @return an Observable that emits a 0L after the {@code initialDelay} and ever increasing numbers after * each {@code period} of time thereafter, while running on the given Scheduler * @see ReactiveX operators documentation: Timer + * @deprecated use {@link #interval(long, long, TimeUnit, Scheduler)} instead */ + @Deprecated public final static Observable timer(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { - return create(new OnSubscribeTimerPeriodically(initialDelay, period, unit, scheduler)); + return interval(initialDelay, period, unit, scheduler); } /** diff --git a/src/perf/java/rx/operators/OperatorSerializePerf.java b/src/perf/java/rx/operators/OperatorSerializePerf.java index 49c32eca5b..cae310b72c 100644 --- a/src/perf/java/rx/operators/OperatorSerializePerf.java +++ b/src/perf/java/rx/operators/OperatorSerializePerf.java @@ -90,7 +90,7 @@ public int getSize() { public void setup(Blackhole bh) { super.setup(bh); - interval = Observable.timer(0, 1, TimeUnit.MILLISECONDS).take(size).map(this); + interval = Observable.interval(0, 1, TimeUnit.MILLISECONDS).take(size).map(this); } @Override public Integer call(Long t1) { diff --git a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java index 3303106d93..0d74cd878b 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java @@ -114,7 +114,7 @@ public Integer call(Long t1) { Observable source2 = source1 .repeat(4) - .zipWith(Observable.timer(0, 10, TimeUnit.MILLISECONDS, Schedulers.newThread()), new Func2() { + .zipWith(Observable.interval(0, 10, TimeUnit.MILLISECONDS, Schedulers.newThread()), new Func2() { @Override public Integer call(Integer t1, Long t2) { return t1; diff --git a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java index c2a9f15dce..e593d30465 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java @@ -820,7 +820,7 @@ public void testWithCombineLatestIssue1717() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final AtomicInteger count = new AtomicInteger(); final int SIZE = 2000; - Observable timer = Observable.timer(0, 1, TimeUnit.MILLISECONDS) + Observable timer = Observable.interval(0, 1, TimeUnit.MILLISECONDS) .observeOn(Schedulers.newThread()) .doOnEach(new Action1>() { diff --git a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java index 59d1ee7de4..fa38d2bdf1 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java @@ -47,7 +47,7 @@ public void setUp() { public void testRefCountAsync() { final AtomicInteger subscribeCount = new AtomicInteger(); final AtomicInteger nextCount = new AtomicInteger(); - Observable r = Observable.timer(0, 5, TimeUnit.MILLISECONDS) + Observable r = Observable.interval(0, 5, TimeUnit.MILLISECONDS) .doOnSubscribe(new Action0() { @Override @@ -183,7 +183,7 @@ public void call(Integer l) { public void testRepeat() { final AtomicInteger subscribeCount = new AtomicInteger(); final AtomicInteger unsubscribeCount = new AtomicInteger(); - Observable r = Observable.timer(0, 1, TimeUnit.MILLISECONDS) + Observable r = Observable.interval(0, 1, TimeUnit.MILLISECONDS) .doOnSubscribe(new Action0() { @Override diff --git a/src/test/java/rx/internal/operators/OnSubscribeTimerTest.java b/src/test/java/rx/internal/operators/OnSubscribeTimerTest.java index 1dda1fa4a9..99c677cedb 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeTimerTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeTimerTest.java @@ -64,7 +64,7 @@ public void testTimerOnce() { @Test public void testTimerPeriodically() { - Subscription c = Observable.timer(100, 100, TimeUnit.MILLISECONDS, scheduler).subscribe(observer); + Subscription c = Observable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler).subscribe(observer); scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); InOrder inOrder = inOrder(observer); @@ -260,7 +260,7 @@ public void onCompleted() { } @Test public void testPeriodicObserverThrows() { - Observable source = Observable.timer(100, 100, TimeUnit.MILLISECONDS, scheduler); + Observable source = Observable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler); InOrder inOrder = inOrder(observer); diff --git a/src/test/java/rx/internal/operators/OperatorBufferTest.java b/src/test/java/rx/internal/operators/OperatorBufferTest.java index 8e0a9b614e..d05c151ee2 100644 --- a/src/test/java/rx/internal/operators/OperatorBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorBufferTest.java @@ -557,7 +557,7 @@ public void bufferWithSizeSkipTake1() { } @Test(timeout = 2000) public void bufferWithTimeTake1() { - Observable source = Observable.timer(40, 40, TimeUnit.MILLISECONDS, scheduler); + Observable source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); Observable> result = source.buffer(100, TimeUnit.MILLISECONDS, scheduler).take(1); @@ -574,7 +574,7 @@ public void bufferWithTimeTake1() { } @Test(timeout = 2000) public void bufferWithTimeSkipTake2() { - Observable source = Observable.timer(40, 40, TimeUnit.MILLISECONDS, scheduler); + Observable source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); Observable> result = source.buffer(100, 60, TimeUnit.MILLISECONDS, scheduler).take(2); @@ -593,8 +593,8 @@ public void bufferWithTimeSkipTake2() { } @Test(timeout = 2000) public void bufferWithBoundaryTake2() { - Observable boundary = Observable.timer(60, 60, TimeUnit.MILLISECONDS, scheduler); - Observable source = Observable.timer(40, 40, TimeUnit.MILLISECONDS, scheduler); + Observable boundary = Observable.interval(60, 60, TimeUnit.MILLISECONDS, scheduler); + Observable source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); Observable> result = source.buffer(boundary).take(2); @@ -615,15 +615,15 @@ public void bufferWithBoundaryTake2() { @Test(timeout = 2000) public void bufferWithStartEndBoundaryTake2() { - Observable start = Observable.timer(61, 61, TimeUnit.MILLISECONDS, scheduler); + Observable start = Observable.interval(61, 61, TimeUnit.MILLISECONDS, scheduler); Func1> end = new Func1>() { @Override public Observable call(Long t1) { - return Observable.timer(100, 100, TimeUnit.MILLISECONDS, scheduler); + return Observable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler); } }; - Observable source = Observable.timer(40, 40, TimeUnit.MILLISECONDS, scheduler); + Observable source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); Observable> result = source.buffer(start, end).take(2); @@ -693,7 +693,7 @@ public void bufferWithTimeThrows() { } @Test public void bufferWithTimeAndSize() { - Observable source = Observable.timer(30, 30, TimeUnit.MILLISECONDS, scheduler); + Observable source = Observable.interval(30, 30, TimeUnit.MILLISECONDS, scheduler); Observable> result = source.buffer(100, TimeUnit.MILLISECONDS, 2, scheduler).take(3); diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index 1f0cc0a892..e505bf0672 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -663,7 +663,7 @@ public void onNext(Long t) { @Test public void testHotOperatorBackpressure() { TestSubscriber ts = new TestSubscriber(); - Observable.timer(0, 1, TimeUnit.MICROSECONDS) + Observable.interval(0, 1, TimeUnit.MICROSECONDS) .observeOn(Schedulers.computation()) .map(new Func1() { @@ -687,7 +687,7 @@ public String call(Long t1) { @Test public void testErrorPropagatesWhenNoOutstandingRequests() { - Observable timer = Observable.timer(0, 1, TimeUnit.MICROSECONDS) + Observable timer = Observable.interval(0, 1, TimeUnit.MICROSECONDS) .doOnEach(new Action1>() { @Override diff --git a/src/test/java/rx/internal/operators/OperatorPublishTest.java b/src/test/java/rx/internal/operators/OperatorPublishTest.java index f6bfaa7e21..a916a5e41c 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishTest.java @@ -247,7 +247,7 @@ public void call() { @Test public void testConnectWithNoSubscriber() { TestScheduler scheduler = new TestScheduler(); - ConnectableObservable co = Observable.timer(10, 10, TimeUnit.MILLISECONDS, scheduler).take(3).publish(); + ConnectableObservable co = Observable.interval(10, 10, TimeUnit.MILLISECONDS, scheduler).take(3).publish(); co.connect(); // Emit 0 scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); diff --git a/src/test/java/rx/internal/producers/ProducersTest.java b/src/test/java/rx/internal/producers/ProducersTest.java index ee746335fd..0e5beacdfa 100644 --- a/src/test/java/rx/internal/producers/ProducersTest.java +++ b/src/test/java/rx/internal/producers/ProducersTest.java @@ -355,10 +355,10 @@ public void testObserverArbiterAsync() { TestScheduler test = Schedulers.test(); @SuppressWarnings("unchecked") List> timers = Arrays.asList( - Observable.timer(100, 100, TimeUnit.MILLISECONDS, test), - Observable.timer(100, 100, TimeUnit.MILLISECONDS, test) + Observable.interval(100, 100, TimeUnit.MILLISECONDS, test), + Observable.interval(100, 100, TimeUnit.MILLISECONDS, test) .map(plus(20)), - Observable.timer(100, 100, TimeUnit.MILLISECONDS, test) + Observable.interval(100, 100, TimeUnit.MILLISECONDS, test) .map(plus(40)) ); From 9585c6e2a54d89f032ab9f2082395eada69b3512 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 19 May 2015 15:40:58 +1000 Subject: [PATCH 276/857] fix Amb backpressure problems --- .../rx/internal/operators/OnSubscribeAmb.java | 102 +++++++++++++----- .../operators/OnSubscribeAmbTest.java | 70 ++++++++++++ 2 files changed, 143 insertions(+), 29 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeAmb.java b/src/main/java/rx/internal/operators/OnSubscribeAmb.java index 534e308653..2ddd0dc820 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeAmb.java +++ b/src/main/java/rx/internal/operators/OnSubscribeAmb.java @@ -269,6 +269,7 @@ private static final class AmbSubscriber extends Subscriber { private final Subscriber subscriber; private final Selection selection; + private boolean chosen; private AmbSubscriber(long requested, Subscriber subscriber, Selection selection) { this.subscriber = subscriber; @@ -282,11 +283,11 @@ private final void requestMore(long n) { } @Override - public void onNext(T args) { + public void onNext(T t) { if (!isSelected()) { return; } - subscriber.onNext(args); + subscriber.onNext(t); } @Override @@ -306,12 +307,17 @@ public void onError(Throwable e) { } private boolean isSelected() { + if (chosen) { + return true; + } if (selection.choice.get() == this) { // fast-path + chosen = true; return true; } else { if (selection.choice.compareAndSet(null, this)) { selection.unsubscribeOthers(this); + chosen = true; return true; } else { // we lost so unsubscribe ... and force cleanup again due to possible race conditions @@ -343,59 +349,97 @@ public void unsubscribeOthers(AmbSubscriber notThis) { } } - - private final Iterable> sources; - private final Selection selection = new Selection(); - + + //give default access instead of private as a micro-optimization + //for access from anonymous classes below + final Iterable> sources; + final Selection selection = new Selection(); + final AtomicReference> choice = selection.choice; + private OnSubscribeAmb(Iterable> sources) { this.sources = sources; } @Override public void call(final Subscriber subscriber) { + + //setup unsubscription of all the subscribers to the sources subscriber.add(Subscriptions.create(new Action0() { @Override public void call() { - if (selection.choice.get() != null) { + AmbSubscriber c; + if ((c = choice.get()) != null) { // there is a single winner so we unsubscribe it - selection.choice.get().unsubscribe(); + c.unsubscribe(); } // if we are racing with others still existing, we'll also unsubscribe them - if(!selection.ambSubscribers.isEmpty()) { - for (AmbSubscriber other : selection.ambSubscribers) { - other.unsubscribe(); - } - selection.ambSubscribers.clear(); - } + // if subscriptions are occurring as this is happening then this call may not + // unsubscribe everything. We protect ourselves though by doing another unsubscribe check + // after the subscription loop below + unsubscribeAmbSubscribers(selection.ambSubscribers); } - + })); + + //need to subscribe to all the sources + for (Observable source : sources) { + if (subscriber.isUnsubscribed()) { + break; + } + AmbSubscriber ambSubscriber = new AmbSubscriber(0, subscriber, selection); + selection.ambSubscribers.add(ambSubscriber); + // check again if choice has been made so can stop subscribing + // if all sources were backpressure aware then this check + // would be pointless given that 0 was requested above from each ambSubscriber + AmbSubscriber c; + if ((c = choice.get()) != null) { + // Already chose one, the rest can be skipped and we can clean up + selection.unsubscribeOthers(c); + return; + } + source.unsafeSubscribe(ambSubscriber); + } + // while subscribing unsubscription may have occurred so we clean up after + if (subscriber.isUnsubscribed()) { + unsubscribeAmbSubscribers(selection.ambSubscribers); + } + subscriber.setProducer(new Producer() { @Override public void request(long n) { - if (selection.choice.get() != null) { + final AmbSubscriber c; + if ((c = choice.get()) != null) { // propagate the request to that single Subscriber that won - selection.choice.get().requestMore(n); + c.requestMore(n); } else { - for (Observable source : sources) { - if (subscriber.isUnsubscribed()) { - break; - } - AmbSubscriber ambSubscriber = new AmbSubscriber(n, subscriber, selection); - selection.ambSubscribers.add(ambSubscriber); - // possible race condition in previous lines ... a choice may have been made so double check (instead of synchronizing) - if (selection.choice.get() != null) { - // Already chose one, the rest can be skipped and we can clean up - selection.unsubscribeOthers(selection.choice.get()); - break; + //propagate the request to all the amb subscribers + for (AmbSubscriber ambSubscriber: selection.ambSubscribers) { + if (!ambSubscriber.isUnsubscribed()) { + // make a best endeavours check to not waste requests + // if first emission has already occurred + if (choice.get() == ambSubscriber) { + ambSubscriber.requestMore(n); + // don't need to request from other subscribers because choice has been made + // and request has gone to choice + return; + } else { + ambSubscriber.requestMore(n); + } } - source.unsafeSubscribe(ambSubscriber); } } } }); } + private static void unsubscribeAmbSubscribers(Collection> ambSubscribers) { + if(!ambSubscribers.isEmpty()) { + for (AmbSubscriber other : ambSubscribers) { + other.unsubscribe(); + } + ambSubscribers.clear(); + } + } } diff --git a/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java b/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java index cb4a307822..76cb40800e 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java @@ -22,6 +22,7 @@ import static rx.internal.operators.OnSubscribeAmb.amb; import java.io.IOException; +import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -36,6 +37,7 @@ import rx.Scheduler; import rx.Subscriber; import rx.functions.Action0; +import rx.functions.Action1; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -219,4 +221,72 @@ public void testBackpressure() { ts.assertNoErrors(); assertEquals(RxRingBuffer.SIZE * 2, ts.getOnNextEvents().size()); } + + + @Test + public void testSubscriptionOnlyHappensOnce() throws InterruptedException { + final AtomicLong count = new AtomicLong(); + Action0 incrementer = new Action0() { + @Override + public void call() { + count.incrementAndGet(); + } + }; + //this aync stream should emit first + Observable o1 = Observable.just(1).doOnSubscribe(incrementer) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + //this stream emits second + Observable o2 = Observable.just(1).doOnSubscribe(incrementer) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + TestSubscriber ts = new TestSubscriber(); + Observable.amb(o1, o2).subscribe(ts); + ts.requestMore(1); + ts.awaitTerminalEvent(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(2, count.get()); + } + + @Test + public void testSecondaryRequestsPropagatedToChildren() throws InterruptedException { + //this aync stream should emit first + Observable o1 = Observable.from(Arrays.asList(1, 2, 3)) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + //this stream emits second + Observable o2 = Observable.from(Arrays.asList(4, 5, 6)) + .delay(200, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(1); + }}; + Observable.amb(o1, o2).subscribe(ts); + // before first emission request 20 more + // this request should suffice to emit all + ts.requestMore(20); + //ensure stream does not hang + ts.awaitTerminalEvent(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void testSynchronousSources() { + // under async subscription the second observable would complete before + // the first but because this is a synchronous subscription to sources + // then second observable does not get subscribed to before first + // subscription completes hence first observable emits result through + // amb + int result = Observable.just(1).doOnNext(new Action1() { + + @Override + public void call(Object t) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // + } + } + }).ambWith(Observable.just(2)).toBlocking().single(); + assertEquals(1, result); + } + } From 76f001bc1211e126aa5ad18bc0acb4fd37cd929e Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 26 May 2015 14:34:59 +1000 Subject: [PATCH 277/857] add factory methods to TestSubscriber --- .../java/rx/observers/TestSubscriber.java | 25 +++++++++++++++++++ .../operators/OnSubscribeRangeTest.java | 10 ++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 56d12b46e2..1a9e723dc1 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -95,6 +95,31 @@ public TestSubscriber() { this(-1); } + @Experimental + public static TestSubscriber create() { + return new TestSubscriber(); + } + + @Experimental + public static TestSubscriber create(long initialRequest) { + return new TestSubscriber(initialRequest); + } + + @Experimental + public static TestSubscriber create(Observer delegate, long initialRequest) { + return new TestSubscriber(delegate, initialRequest); + } + + @Experimental + public static TestSubscriber create(Subscriber delegate) { + return new TestSubscriber(delegate); + } + + @Experimental + public static TestSubscriber create(Observer delegate) { + return new TestSubscriber(delegate); + } + @Override public void onStart() { if (initialRequest >= 0) { diff --git a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java index aea7a36730..6d4d97d019 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java @@ -106,7 +106,7 @@ public void testRangeWithOverflow5() { @Test public void testBackpressureViaRequest() { OnSubscribeRange o = new OnSubscribeRange(1, RxRingBuffer.SIZE); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = TestSubscriber.create(); ts.assertReceivedOnNext(Collections. emptyList()); ts.requestMore(1); o.call(ts); @@ -127,7 +127,7 @@ public void testNoBackpressure() { } OnSubscribeRange o = new OnSubscribeRange(1, list.size()); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = TestSubscriber.create(); ts.assertReceivedOnNext(Collections. emptyList()); ts.requestMore(Long.MAX_VALUE); // infinite o.call(ts); @@ -137,7 +137,7 @@ public void testNoBackpressure() { void testWithBackpressureOneByOne(int start) { Observable source = Observable.range(start, 100); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = TestSubscriber.create(); ts.requestMore(1); source.subscribe(ts); @@ -152,7 +152,7 @@ void testWithBackpressureOneByOne(int start) { void testWithBackpressureAllAtOnce(int start) { Observable source = Observable.range(start, 100); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = TestSubscriber.create(); ts.requestMore(100); source.subscribe(ts); @@ -179,7 +179,7 @@ public void testWithBackpressureAllAtOnce() { public void testWithBackpressureRequestWayMore() { Observable source = Observable.range(50, 100); - TestSubscriber ts = new TestSubscriber(); + TestSubscriber ts = TestSubscriber.create(); ts.requestMore(150); source.subscribe(ts); From fd9b6bcbc332c9bbf8354e6d3318209c0fc19957 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 26 May 2015 09:15:33 +0200 Subject: [PATCH 278/857] Fixed multiple calls to onStart. --- .../internal/operators/OnSubscribeDefer.java | 17 +++++++- .../OnSubscribeDelaySubscription.java | 20 +++++++-- ...ubscribeDelaySubscriptionWithSelector.java | 18 ++++++-- .../internal/operators/OnSubscribeUsing.java | 26 ++++++++---- .../operators/OperatorDoOnSubscribe.java | 15 ++++++- .../operators/OperatorDoOnUnsubscribe.java | 20 ++++++++- .../internal/operators/OperatorGroupBy.java | 4 +- .../internal/operators/OperatorMulticast.java | 17 +++++++- .../OperatorOnErrorResumeNextViaFunction.java | 41 ++++++++++++++----- .../java/rx/observers/TestSubscriber.java | 2 - .../operators/OperatorPublishTest.java | 3 +- 11 files changed, 145 insertions(+), 38 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeDefer.java b/src/main/java/rx/internal/operators/OnSubscribeDefer.java index b0dd8ba0f0..4a6434140c 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDefer.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDefer.java @@ -38,7 +38,7 @@ public OnSubscribeDefer(Func0> observableFacto } @Override - public void call(Subscriber s) { + public void call(final Subscriber s) { Observable o; try { o = observableFactory.call(); @@ -46,7 +46,20 @@ public void call(Subscriber s) { s.onError(t); return; } - o.unsafeSubscribe(s); + o.unsafeSubscribe(new Subscriber(s) { + @Override + public void onNext(T t) { + s.onNext(t); + } + @Override + public void onError(Throwable e) { + s.onError(e); + } + @Override + public void onCompleted() { + s.onCompleted(); + } + }); } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java index d60bdb73a6..95036d399e 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java @@ -16,11 +16,10 @@ package rx.internal.operators; import java.util.concurrent.TimeUnit; -import rx.Observable; + +import rx.*; import rx.Observable.OnSubscribe; -import rx.Scheduler; import rx.Scheduler.Worker; -import rx.Subscriber; import rx.functions.Action0; /** @@ -50,7 +49,20 @@ public void call(final Subscriber s) { @Override public void call() { if (!s.isUnsubscribed()) { - source.unsafeSubscribe(s); + source.unsafeSubscribe(new Subscriber(s) { + @Override + public void onNext(T t) { + s.onNext(t); + } + @Override + public void onError(Throwable e) { + s.onError(e); + } + @Override + public void onCompleted() { + s.onCompleted(); + } + }); } } }, time, unit); diff --git a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java index 64e0997474..d6b2f0ad2c 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java @@ -15,9 +15,8 @@ */ package rx.internal.operators; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Subscriber; import rx.functions.Func0; /** @@ -43,7 +42,20 @@ public void call(final Subscriber child) { @Override public void onCompleted() { // subscribe to actual source - source.unsafeSubscribe(child); + source.unsafeSubscribe(new Subscriber(child) { + @Override + public void onNext(T t) { + child.onNext(t); + } + @Override + public void onError(Throwable e) { + child.onError(e); + } + @Override + public void onCompleted() { + child.onCompleted(); + } + }); } @Override diff --git a/src/main/java/rx/internal/operators/OnSubscribeUsing.java b/src/main/java/rx/internal/operators/OnSubscribeUsing.java index 8c29d632d9..7470a65dc8 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeUsing.java +++ b/src/main/java/rx/internal/operators/OnSubscribeUsing.java @@ -18,15 +18,10 @@ import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.Subscription; import rx.exceptions.CompositeException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func0; -import rx.functions.Func1; +import rx.functions.*; /** * Constructs an observable sequence that depends on a resource object. @@ -48,7 +43,7 @@ public OnSubscribeUsing(Func0 resourceFactory, } @Override - public void call(Subscriber subscriber) { + public void call(final Subscriber subscriber) { try { @@ -73,7 +68,20 @@ public void call(Subscriber subscriber) { observable = source; try { // start - observable.unsafeSubscribe(subscriber); + observable.unsafeSubscribe(new Subscriber(subscriber) { + @Override + public void onNext(T t) { + subscriber.onNext(t); + } + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + }); } catch (Throwable e) { Throwable disposeError = disposeEagerlyIfRequested(disposeOnceOnly); if (disposeError != null) diff --git a/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java b/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java index 391e937d1e..b7999c2b5c 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java @@ -39,6 +39,19 @@ public Subscriber call(final Subscriber child) { subscribe.call(); // Pass through since this operator is for notification only, there is // no change to the stream whatsoever. - return child; + return new Subscriber(child) { + @Override + public void onNext(T t) { + child.onNext(t); + } + @Override + public void onError(Throwable e) { + child.onError(e); + } + @Override + public void onCompleted() { + child.onCompleted(); + } + }; } } diff --git a/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java b/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java index 480b31f38c..396012c2eb 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java @@ -16,7 +16,7 @@ package rx.internal.operators; import rx.Observable.Operator; -import rx.Subscriber; +import rx.*; import rx.functions.Action0; import rx.subscriptions.Subscriptions; @@ -41,6 +41,22 @@ public Subscriber call(final Subscriber child) { // Pass through since this operator is for notification only, there is // no change to the stream whatsoever. - return child; + return new Subscriber(child) { + @Override + public void onStart() { + } + @Override + public void onNext(T t) { + child.onNext(t); + } + @Override + public void onError(Throwable e) { + child.onError(e); + } + @Override + public void onCompleted() { + child.onCompleted(); + } + }; } } diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 93631569df..3d8f45067c 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -259,7 +259,9 @@ public void call() { } }).unsafeSubscribe(new Subscriber(o) { - + @Override + public void onStart() { + } @Override public void onCompleted() { o.onCompleted(); diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java index 4d5d10f4f3..8de1b93984 100644 --- a/src/main/java/rx/internal/operators/OperatorMulticast.java +++ b/src/main/java/rx/internal/operators/OperatorMulticast.java @@ -128,8 +128,21 @@ public void call() { guardedSubscription = gs.get(); // register any subscribers that are waiting with this new subject - for(Subscriber s : waitingForConnect) { - subject.unsafeSubscribe(s); + for(final Subscriber s : waitingForConnect) { + subject.unsafeSubscribe(new Subscriber(s) { + @Override + public void onNext(R t) { + s.onNext(t); + } + @Override + public void onError(Throwable e) { + s.onError(e); + } + @Override + public void onCompleted() { + s.onCompleted(); + } + }); } // clear the waiting list as any new ones that come in after leaving this synchronized block will go direct to the Subject waitingForConnect.clear(); diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java index 96a3c5c170..70380a1a2b 100644 --- a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java +++ b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java @@ -15,13 +15,13 @@ */ package rx.internal.operators; -import rx.Observable; -import rx.Producer; +import rx.*; import rx.Observable.Operator; -import rx.Subscriber; import rx.exceptions.Exceptions; import rx.functions.Func1; +import rx.internal.producers.ProducerArbiter; import rx.plugins.RxJavaPlugins; +import rx.subscriptions.SerialSubscription; /** * Instruct an Observable to pass control to another Observable (the return value of a function) @@ -51,6 +51,8 @@ public OperatorOnErrorResumeNextViaFunction(Func1 call(final Subscriber child) { + final ProducerArbiter pa = new ProducerArbiter(); + final SerialSubscription ssub = new SerialSubscription(); Subscriber parent = new Subscriber() { private boolean done = false; @@ -74,8 +76,28 @@ public void onError(Throwable e) { try { RxJavaPlugins.getInstance().getErrorHandler().handleError(e); unsubscribe(); + Subscriber next = new Subscriber() { + @Override + public void onNext(T t) { + child.onNext(t); + } + @Override + public void onError(Throwable e) { + child.onError(e); + } + @Override + public void onCompleted() { + child.onCompleted(); + } + @Override + public void setProducer(Producer producer) { + pa.setProducer(producer); + } + }; + ssub.set(next); + Observable resume = resumeFunction.call(e); - resume.unsafeSubscribe(child); + resume.unsafeSubscribe(next); } catch (Throwable e2) { child.onError(e2); } @@ -91,16 +113,13 @@ public void onNext(T t) { @Override public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - @Override - public void request(long n) { - producer.request(n); - } - }); + pa.setProducer(producer); } }; - child.add(parent); + child.add(ssub); + ssub.set(parent); + child.setProducer(pa); return parent; } diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 56d12b46e2..027221b805 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -99,8 +99,6 @@ public TestSubscriber() { public void onStart() { if (initialRequest >= 0) { requestMore(initialRequest); - } else { - super.onStart(); } } diff --git a/src/test/java/rx/internal/operators/OperatorPublishTest.java b/src/test/java/rx/internal/operators/OperatorPublishTest.java index f6bfaa7e21..ab815ceb7d 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishTest.java @@ -226,7 +226,8 @@ public void call() { public void call() { child1Unsubscribed.set(true); } - }).take(5).subscribe(ts1); + }).take(5) + .subscribe(ts1); ts1.awaitTerminalEvent(); ts2.awaitTerminalEvent(); From 5db7b94162a97c1538066ec4ded322aac6ce56b3 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 19 May 2015 10:26:36 +1000 Subject: [PATCH 279/857] fix OperatorConcat request race conditions with ProducerArbiter --- .../rx/internal/operators/OperatorConcat.java | 60 ++++++++++--------- .../internal/producers/ProducerArbiter.java | 2 +- .../operators/OperatorConcatTest.java | 31 +++++++++- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index 8e8514b9ef..e91e669bba 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -24,6 +24,7 @@ import rx.Producer; import rx.Subscriber; import rx.functions.Action0; +import rx.internal.producers.ProducerArbiter; import rx.observers.SerializedSubscriber; import rx.subscriptions.SerialSubscription; import rx.subscriptions.Subscriptions; @@ -85,17 +86,19 @@ static final class ConcatSubscriber extends Subscriber WIP_UPDATER = AtomicIntegerFieldUpdater.newUpdater(ConcatSubscriber.class, "wip"); + static final AtomicIntegerFieldUpdater WIP = AtomicIntegerFieldUpdater.newUpdater(ConcatSubscriber.class, "wip"); - // accessed by REQUESTED_UPDATER + // accessed by REQUESTED private volatile long requested; @SuppressWarnings("rawtypes") - private static final AtomicLongFieldUpdater REQUESTED_UPDATER = AtomicLongFieldUpdater.newUpdater(ConcatSubscriber.class, "requested"); + private static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(ConcatSubscriber.class, "requested"); + 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 @@ -113,32 +116,27 @@ public void onStart() { } private void requestFromChild(long n) { + if (n <=0) return; // we track 'requested' so we know whether we should subscribe the next or not - ConcatInnerSubscriber actualSubscriber = currentSubscriber; - if (n > 0 && BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER, this, n) == 0) { - if (actualSubscriber == null && wip > 0) { + long previous = BackpressureUtils.getAndAddRequest(REQUESTED, this, n); + arbiter.request(n); + if (previous == 0) { + if (currentSubscriber == null && wip > 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(); - // return here as we don't want to do the requestMore logic below (which would double request) - return; } } - - if (actualSubscriber != null) { - // otherwise we are just passing it through to the currentSubscriber - actualSubscriber.requestMore(n); - } } private void decrementRequested() { - REQUESTED_UPDATER.decrementAndGet(this); + REQUESTED.decrementAndGet(this); } @Override public void onNext(Observable t) { queue.add(nl.next(t)); - if (WIP_UPDATER.getAndIncrement(this) == 0) { + if (WIP.getAndIncrement(this) == 0) { subscribeNext(); } } @@ -152,14 +150,15 @@ public void onError(Throwable e) { @Override public void onCompleted() { queue.add(nl.completed()); - if (WIP_UPDATER.getAndIncrement(this) == 0) { + if (WIP.getAndIncrement(this) == 0) { subscribeNext(); } } + void completeInner() { currentSubscriber = null; - if (WIP_UPDATER.decrementAndGet(this) > 0) { + if (WIP.decrementAndGet(this) > 0) { subscribeNext(); } request(1); @@ -172,7 +171,7 @@ void subscribeNext() { child.onCompleted(); } else if (o != null) { Observable obs = nl.getValue(o); - currentSubscriber = new ConcatInnerSubscriber(this, child, requested); + currentSubscriber = new ConcatInnerSubscriber(this, child, arbiter); current.set(currentSubscriber); obs.unsafeSubscribe(currentSubscriber); } @@ -193,27 +192,25 @@ static class ConcatInnerSubscriber extends Subscriber { @SuppressWarnings("unused") private volatile int once = 0; @SuppressWarnings("rawtypes") - private final static AtomicIntegerFieldUpdater ONCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(ConcatInnerSubscriber.class, "once"); + private final static AtomicIntegerFieldUpdater ONCE = AtomicIntegerFieldUpdater.newUpdater(ConcatInnerSubscriber.class, "once"); + private final ProducerArbiter arbiter; - public ConcatInnerSubscriber(ConcatSubscriber parent, Subscriber child, long initialRequest) { + public ConcatInnerSubscriber(ConcatSubscriber parent, Subscriber child, ProducerArbiter arbiter) { this.parent = parent; this.child = child; - request(initialRequest); - } - - void requestMore(long n) { - request(n); + this.arbiter = arbiter; } - + @Override public void onNext(T t) { - parent.decrementRequested(); child.onNext(t); + parent.decrementRequested(); + arbiter.produced(1); } @Override public void onError(Throwable e) { - if (ONCE_UPDATER.compareAndSet(this, 0, 1)) { + if (ONCE.compareAndSet(this, 0, 1)) { // terminal error through parent so everything gets cleaned up, including this inner parent.onError(e); } @@ -221,11 +218,16 @@ public void onError(Throwable e) { @Override public void onCompleted() { - if (ONCE_UPDATER.compareAndSet(this, 0, 1)) { + if (ONCE.compareAndSet(this, 0, 1)) { // terminal completion to parent so it continues to the next parent.completeInner(); } } + + @Override + public void setProducer(Producer producer) { + arbiter.setProducer(producer); + } }; } diff --git a/src/main/java/rx/internal/producers/ProducerArbiter.java b/src/main/java/rx/internal/producers/ProducerArbiter.java index d90a575447..b23904103e 100644 --- a/src/main/java/rx/internal/producers/ProducerArbiter.java +++ b/src/main/java/rx/internal/producers/ProducerArbiter.java @@ -95,7 +95,7 @@ public void produced(long n) { if (r != Long.MAX_VALUE) { long u = r - n; if (u < 0) { - throw new IllegalStateException(); + throw new IllegalStateException("more items arrived than were requested"); } requested = u; } diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 75bfee65f4..c04b6dd910 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -36,9 +36,7 @@ import org.mockito.InOrder; import rx.Observable.OnSubscribe; -import rx.Scheduler.Worker; import rx.*; -import rx.functions.Action0; import rx.functions.Func1; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; @@ -795,4 +793,33 @@ public void onNext(Integer t) { assertTrue(completed.get()); } + @Test//(timeout = 100000) + public void concatMapRangeAsyncLoopIssue2876() { + final long durationSeconds = 2; + final long startTime = System.currentTimeMillis(); + for (int i = 0;; i++) { + //only run this for a max of ten seconds + if (System.currentTimeMillis()-startTime > TimeUnit.SECONDS.toMillis(durationSeconds)) + return; + if (i % 1000 == 0) { + System.out.println("concatMapRangeAsyncLoop > " + i); + } + TestSubscriber ts = new TestSubscriber(); + Observable.range(0, 1000) + .concatMap(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.from(Arrays.asList(t)); + } + }) + .observeOn(Schedulers.computation()).subscribe(ts); + + ts.awaitTerminalEvent(2500, TimeUnit.MILLISECONDS); + ts.assertTerminalEvent(); + ts.assertNoErrors(); + assertEquals(1000, ts.getOnNextEvents().size()); + assertEquals((Integer)999, ts.getOnNextEvents().get(999)); + } + } + } From 80a0b017c4c6b1c7f7be41b3a6b7b03e65a50cd6 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 27 May 2015 21:48:35 +1000 Subject: [PATCH 280/857] make OperatorSerializeTest.testMultiThreadedWithNPEinMiddle fail less often --- .../operators/OperatorSerializeTest.java | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorSerializeTest.java b/src/test/java/rx/internal/operators/OperatorSerializeTest.java index 5cabe0860f..faed052beb 100644 --- a/src/test/java/rx/internal/operators/OperatorSerializeTest.java +++ b/src/test/java/rx/internal/operators/OperatorSerializeTest.java @@ -120,31 +120,38 @@ public void testMultiThreadedWithNPE() { @Test public void testMultiThreadedWithNPEinMiddle() { - TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); - Observable w = Observable.create(onSubscribe); - - BusyObserver busyobserver = new BusyObserver(); - - w.serialize().subscribe(busyobserver); - onSubscribe.waitToFinish(); - - System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); - // this should not be the full number of items since the error should stop it before it completes all 9 - System.out.println("onNext count: " + busyobserver.onNextCount.get()); - assertTrue(busyobserver.onNextCount.get() < 9); - assertTrue(busyobserver.onError); - // no onCompleted because onError was invoked - assertFalse(busyobserver.onCompleted); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - // verify(s, times(1)).unsubscribe(); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyobserver.maxConcurrentThreads.get()); + boolean lessThan9 = false; + for (int i = 0; i < 3; i++) { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + // this should not always be the full number of items since the error should (very often) + // stop it before it completes all 9 + System.out.println("onNext count: " + busyobserver.onNextCount.get()); + if (busyobserver.onNextCount.get() < 9) { + lessThan9 = true; + } + assertTrue(busyobserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + assertTrue(lessThan9); } - + /** * A thread that will pass data to onNext */ @@ -276,6 +283,7 @@ public TestMultiThreadedObservable(String... values) { @Override public void call(final Subscriber observer) { System.out.println("TestMultiThreadedObservable subscribed to ..."); + final NullPointerException npe = new NullPointerException(); t = new Thread(new Runnable() { @Override @@ -290,11 +298,12 @@ public void run() { threadsRunning.incrementAndGet(); try { // perform onNext call - System.out.println("TestMultiThreadedObservable onNext: " + s); if (s == null) { + System.out.println("TestMultiThreadedObservable onNext: null"); // force an error - throw new NullPointerException(); - } + throw npe; + } else + System.out.println("TestMultiThreadedObservable onNext: " + s); observer.onNext(s); // capture 'maxThreads' int concurrentThreads = threadsRunning.get(); From 3faea61eaf17c4cf61c9cfb30a70f757fbc71c8f Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 28 May 2015 04:38:01 +1000 Subject: [PATCH 281/857] implement backpressure for OperatorAll and include last value in cause if exception occurs --- .../rx/internal/operators/OperatorAll.java | 27 ++++++---- .../internal/operators/OperatorAllTest.java | 50 +++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorAll.java b/src/main/java/rx/internal/operators/OperatorAll.java index 24619e3b20..3f78eeff88 100644 --- a/src/main/java/rx/internal/operators/OperatorAll.java +++ b/src/main/java/rx/internal/operators/OperatorAll.java @@ -17,7 +17,10 @@ import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; +import rx.internal.producers.SingleDelayedProducer; /** * Returns an Observable that emits a Boolean that indicates whether all items emitted by an @@ -34,21 +37,27 @@ public OperatorAll(Func1 predicate) { @Override public Subscriber call(final Subscriber child) { + final SingleDelayedProducer producer = new SingleDelayedProducer(child); Subscriber s = new Subscriber() { boolean done; @Override public void onNext(T t) { - boolean result = predicate.call(t); + Boolean result; + try { + result = predicate.call(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + onError(OnErrorThrowable.addValueAsLastCause(e, t)); + return; + } if (!result && !done) { done = true; - child.onNext(false); - child.onCompleted(); + producer.setValue(false); unsubscribe(); - } else { - // if we drop values we must replace them upstream as downstream won't receive and request more - request(1); - } + } + // note that don't need to request more of upstream because this subscriber + // defaults to requesting Long.MAX_VALUE } @Override @@ -60,12 +69,12 @@ public void onError(Throwable e) { public void onCompleted() { if (!done) { done = true; - child.onNext(true); - child.onCompleted(); + producer.setValue(true); } } }; child.add(s); + child.setProducer(producer); return s; } } diff --git a/src/test/java/rx/internal/operators/OperatorAllTest.java b/src/test/java/rx/internal/operators/OperatorAllTest.java index 92a2dfa056..1fd84bc129 100644 --- a/src/test/java/rx/internal/operators/OperatorAllTest.java +++ b/src/test/java/rx/internal/operators/OperatorAllTest.java @@ -19,12 +19,14 @@ import static org.mockito.Mockito.*; import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import org.junit.Test; import rx.*; import rx.functions.Func1; +import rx.observers.TestSubscriber; public class OperatorAllTest { @@ -128,4 +130,52 @@ public Observable call(Boolean t1) { }); assertEquals((Object)2, source.toBlocking().first()); } + + @Test + public void testBackpressureIfNoneRequestedNoneShouldBeDelivered() { + TestSubscriber ts = new TestSubscriber(0); + Observable.empty().all(new Func1() { + @Override + public Boolean call(Object t1) { + return false; + } + }).subscribe(ts); + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + } + + @Test + public void testBackpressureIfOneRequestedOneShouldBeDelivered() { + TestSubscriber ts = new TestSubscriber(1); + Observable.empty().all(new Func1() { + @Override + public Boolean call(Object object) { + return false; + } + }).subscribe(ts); + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertCompleted(); + ts.assertValue(true); + } + + @Test + public void testPredicateThrowsExceptionAndValueInCauseMessage() { + TestSubscriber ts = new TestSubscriber(0); + final IllegalArgumentException ex = new IllegalArgumentException(); + Observable.just("Boo!").all(new Func1() { + @Override + public Boolean call(Object object) { + throw ex; + } + }).subscribe(ts); + ts.assertTerminalEvent(); + ts.assertNoValues(); + ts.assertNotCompleted(); + List errors = ts.getOnErrorEvents(); + assertEquals(1, errors.size()); + assertEquals(ex, errors.get(0)); + assertTrue(ex.getCause().getMessage().contains("Boo!")); + } } From d8f9e8620cd4c52dce037bb205840a1c9853f5b6 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 28 May 2015 19:53:07 +1000 Subject: [PATCH 282/857] fix skip race conditions and request overflow --- .../rx/internal/operators/OperatorSkip.java | 17 ++------ .../internal/operators/OperatorSkipTest.java | 40 ++++++++++++++++++- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSkip.java b/src/main/java/rx/internal/operators/OperatorSkip.java index 2598145e84..878898aaba 100644 --- a/src/main/java/rx/internal/operators/OperatorSkip.java +++ b/src/main/java/rx/internal/operators/OperatorSkip.java @@ -15,6 +15,8 @@ */ package rx.internal.operators; +import java.util.concurrent.atomic.AtomicBoolean; + import rx.Observable; import rx.Producer; import rx.Subscriber; @@ -63,19 +65,8 @@ public void onNext(T t) { @Override public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - - @Override - public void request(long n) { - if (n == Long.MAX_VALUE) { - // infinite so leave it alone - producer.request(n); - } else if (n > 0) { - // add the skip num to the requested amount, since we'll skip everything and then emit to the buffer downstream - producer.request(n + (toSkip - skipped)); - } - } - }); + child.setProducer(producer); + producer.request(toSkip); } }; diff --git a/src/test/java/rx/internal/operators/OperatorSkipTest.java b/src/test/java/rx/internal/operators/OperatorSkipTest.java index 9f4b75d5a7..0e9ca9367e 100644 --- a/src/test/java/rx/internal/operators/OperatorSkipTest.java +++ b/src/test/java/rx/internal/operators/OperatorSkipTest.java @@ -15,17 +15,23 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; 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.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + import org.junit.Test; import rx.Observable; import rx.Observer; -import rx.internal.operators.OperatorSkip; +import rx.functions.Action1; +import rx.observers.TestSubscriber; public class OperatorSkipTest { @@ -144,4 +150,36 @@ public void testSkipError() { verify(observer, never()).onCompleted(); } + + @Test + public void testBackpressureMultipleSmallAsyncRequests() throws InterruptedException { + final AtomicLong requests = new AtomicLong(0); + TestSubscriber ts = new TestSubscriber(0); + Observable.interval(100, TimeUnit.MILLISECONDS) + .doOnRequest(new Action1() { + @Override + public void call(Long n) { + requests.addAndGet(n); + } + }).skip(4).subscribe(ts); + Thread.sleep(100); + ts.requestMore(1); + ts.requestMore(1); + Thread.sleep(100); + ts.unsubscribe(); + ts.assertUnsubscribed(); + ts.assertNoErrors(); + assertEquals(6, requests.get()); + } + + @Test + public void testRequestOverflowDoesNotOccur() { + TestSubscriber ts = new TestSubscriber(Long.MAX_VALUE-1); + Observable.range(1, 10).skip(5).subscribe(ts); + ts.assertTerminalEvent(); + ts.assertCompleted(); + ts.assertNoErrors(); + assertEquals(Arrays.asList(6,7,8,9,10), ts.getOnNextEvents()); + } + } From 142f31e58c23125a04260b4c3977ea3d68ea0ff2 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 29 May 2015 07:56:43 +1000 Subject: [PATCH 283/857] add backpressure support for OperatorAny and include last value in exception cause --- .../rx/internal/operators/OperatorAny.java | 29 +++++++---- .../internal/operators/OperatorAnyTest.java | 50 +++++++++++++++++++ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorAny.java b/src/main/java/rx/internal/operators/OperatorAny.java index 47e9e017e4..7bd9d3f00b 100644 --- a/src/main/java/rx/internal/operators/OperatorAny.java +++ b/src/main/java/rx/internal/operators/OperatorAny.java @@ -19,7 +19,10 @@ import rx.Observable; import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; +import rx.internal.producers.SingleDelayedProducer; /** * Returns an {@link Observable} that emits true if any element of @@ -36,6 +39,7 @@ public OperatorAny(Func1 predicate, boolean returnOnEmpty) { @Override public Subscriber call(final Subscriber child) { + final SingleDelayedProducer producer = new SingleDelayedProducer(child); Subscriber s = new Subscriber() { boolean hasElements; boolean done; @@ -43,16 +47,21 @@ public Subscriber call(final Subscriber child) { @Override public void onNext(T t) { hasElements = true; - boolean result = predicate.call(t); + boolean result; + try { + result = predicate.call(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + onError(OnErrorThrowable.addValueAsLastCause(e, t)); + return; + } if (result && !done) { done = true; - child.onNext(!returnOnEmpty); - child.onCompleted(); + producer.setValue(!returnOnEmpty); unsubscribe(); - } else { - // if we drop values we must replace them upstream as downstream won't receive and request more - request(1); - } + } + // note that don't need to request more of upstream because this subscriber + // defaults to requesting Long.MAX_VALUE } @Override @@ -65,16 +74,16 @@ public void onCompleted() { if (!done) { done = true; if (hasElements) { - child.onNext(false); + producer.setValue(false); } else { - child.onNext(returnOnEmpty); + producer.setValue(returnOnEmpty); } - child.onCompleted(); } } }; child.add(s); + child.setProducer(producer); return s; } } diff --git a/src/test/java/rx/internal/operators/OperatorAnyTest.java b/src/test/java/rx/internal/operators/OperatorAnyTest.java index 6a9e56ce40..b48928d800 100644 --- a/src/test/java/rx/internal/operators/OperatorAnyTest.java +++ b/src/test/java/rx/internal/operators/OperatorAnyTest.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.*; import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import org.junit.Test; @@ -26,6 +27,7 @@ import rx.*; import rx.functions.Func1; import rx.internal.util.UtilityFunctions; +import rx.observers.TestSubscriber; public class OperatorAnyTest { @@ -220,4 +222,52 @@ public Observable call(Boolean t1) { }); assertEquals((Object)2, source.toBlocking().first()); } + + @Test + public void testBackpressureIfNoneRequestedNoneShouldBeDelivered() { + TestSubscriber ts = new TestSubscriber(0); + Observable.just(1).exists(new Func1() { + @Override + public Boolean call(Object t1) { + return true; + } + }).subscribe(ts); + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + } + + @Test + public void testBackpressureIfOneRequestedOneShouldBeDelivered() { + TestSubscriber ts = new TestSubscriber(1); + Observable.just(1).exists(new Func1() { + @Override + public Boolean call(Object object) { + return true; + } + }).subscribe(ts); + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertCompleted(); + ts.assertValue(true); + } + + @Test + public void testPredicateThrowsExceptionAndValueInCauseMessage() { + TestSubscriber ts = new TestSubscriber(0); + final IllegalArgumentException ex = new IllegalArgumentException(); + Observable.just("Boo!").exists(new Func1() { + @Override + public Boolean call(Object object) { + throw ex; + } + }).subscribe(ts); + ts.assertTerminalEvent(); + ts.assertNoValues(); + ts.assertNotCompleted(); + List errors = ts.getOnErrorEvents(); + assertEquals(1, errors.size()); + assertEquals(ex, errors.get(0)); + assertTrue(ex.getCause().getMessage().contains("Boo!")); + } } From 85a035d91473bd96a330cd8f3b7be23990ca4696 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 29 May 2015 09:17:30 +1000 Subject: [PATCH 284/857] prevent OperatorTake from requesting more than needed --- .../rx/internal/operators/OperatorTake.java | 25 +++++++++++++----- .../internal/operators/OperatorTakeTest.java | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorTake.java b/src/main/java/rx/internal/operators/OperatorTake.java index a53b293a9f..0cc42b88ef 100644 --- a/src/main/java/rx/internal/operators/OperatorTake.java +++ b/src/main/java/rx/internal/operators/OperatorTake.java @@ -15,6 +15,8 @@ */ package rx.internal.operators; +import java.util.concurrent.atomic.AtomicLong; + import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; @@ -78,15 +80,24 @@ public void onNext(T i) { @Override public void setProducer(final Producer producer) { child.setProducer(new Producer() { - + + // keeps track of requests up to maximum of `limit` + final AtomicLong requested = new AtomicLong(0); + @Override public void request(long n) { - if (!completed) { - long c = limit - count; - if (n < c) { - producer.request(n); - } else { - producer.request(c); + if (n >0 && !completed) { + // because requests may happen concurrently use a CAS loop to + // ensure we only request as much as needed, no more no less + while (true) { + long r = requested.get(); + long c = Math.min(n, limit - r); + if (c == 0) + break; + else if (requested.compareAndSet(r, r + c)) { + producer.request(c); + break; + } } } } diff --git a/src/test/java/rx/internal/operators/OperatorTakeTest.java b/src/test/java/rx/internal/operators/OperatorTakeTest.java index 6590e73c71..111eb6abbd 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeTest.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -388,4 +389,29 @@ public void call(Integer t1) { latch.await(); assertNull(exception.get()); } + + @Test + public void testDoesntRequestMoreThanNeededFromUpstream() throws InterruptedException { + final AtomicLong requests = new AtomicLong(); + TestSubscriber ts = new TestSubscriber(0); + Observable.interval(100, TimeUnit.MILLISECONDS) + // + .doOnRequest(new Action1() { + @Override + public void call(Long n) { + requests.addAndGet(n); + }}) + // + .take(2) + // + .subscribe(ts); + Thread.sleep(50); + ts.requestMore(1); + ts.requestMore(1); + ts.requestMore(1); + ts.awaitTerminalEvent(); + ts.assertCompleted(); + ts.assertNoErrors(); + assertEquals(2,requests.get()); + } } From 0cf708256f99843444a93b8de950641fcd74bda7 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 30 May 2015 11:34:58 +1000 Subject: [PATCH 285/857] improve Subscriber readability and don't perform unnecessary test in request method --- src/main/java/rx/Subscriber.java | 96 ++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 0c26201552..1767237b25 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -31,20 +31,23 @@ * the type of items the Subscriber expects to observe */ public abstract class Subscriber implements Observer, Subscription { + + // represents requested not set yet + private static final Long NOT_SET = Long.MIN_VALUE; - private final SubscriptionList cs; - private final Subscriber op; + private final SubscriptionList subscriptions; + private final Subscriber subscriber; /* protected by `this` */ - private Producer p; + private Producer producer; /* protected by `this` */ - private long requested = Long.MIN_VALUE; // default to not set + private long requested = NOT_SET; // default to not set protected Subscriber() { this(null, false); } - protected Subscriber(Subscriber op) { - this(op, true); + protected Subscriber(Subscriber subscriber) { + this(subscriber, true); } /** @@ -53,15 +56,15 @@ protected Subscriber(Subscriber op) { *

* To retain the chaining of subscribers, add the created instance to {@code op} via {@link #add}. * - * @param op + * @param subscriber * the other Subscriber * @param shareSubscriptions * {@code true} to share the subscription list in {@code op} with this instance * @since 1.0.6 */ - protected Subscriber(Subscriber op, boolean shareSubscriptions) { - this.op = op; - this.cs = shareSubscriptions && op != null ? op.cs : new SubscriptionList(); + protected Subscriber(Subscriber subscriber, boolean shareSubscriptions) { + this.subscriber = subscriber; + this.subscriptions = shareSubscriptions && subscriber != null ? subscriber.subscriptions : new SubscriptionList(); } /** @@ -73,12 +76,12 @@ protected Subscriber(Subscriber op, boolean shareSubscriptions) { * the {@code Subscription} to add */ public final void add(Subscription s) { - cs.add(s); + subscriptions.add(s); } @Override public final void unsubscribe() { - cs.unsubscribe(); + subscriptions.unsubscribe(); } /** @@ -88,7 +91,7 @@ public final void unsubscribe() { */ @Override public final boolean isUnsubscribed() { - return cs.isUnsubscribed(); + return subscriptions.isUnsubscribed(); } /** @@ -124,57 +127,64 @@ protected final void request(long n) { if (n < 0) { throw new IllegalArgumentException("number requested cannot be negative: " + n); } - Producer shouldRequest = null; + + // if producer is set then we will request from it + // otherwise we increase the requested count by n + Producer producerToRequestFrom = null; synchronized (this) { - if (p != null) { - shouldRequest = p; - } else if (requested == Long.MIN_VALUE) { - requested = n; - } else { - final long total = requested + n; - // check if overflow occurred - if (total < 0) { - requested = Long.MAX_VALUE; - } else { - requested = total; - } + if (producer != null) { + producerToRequestFrom = producer; + } else { + addToRequested(n); + return; } } - // after releasing lock - if (shouldRequest != null) { - shouldRequest.request(n); - } + // after releasing lock (we should not make requests holding a lock) + producerToRequestFrom.request(n); } + private void addToRequested(long n) { + if (requested == NOT_SET) { + requested = n; + } else { + final long total = requested + n; + // check if overflow occurred + if (total < 0) { + requested = Long.MAX_VALUE; + } else { + requested = total; + } + } + } + /** * @warn javadoc description missing * @warn param producer not described - * @param producer + * @param p */ - public void setProducer(Producer producer) { + public void setProducer(Producer p) { long toRequest; - boolean setProducer = false; + boolean passToSubscriber = false; synchronized (this) { toRequest = requested; - p = producer; - if (op != null) { + producer = p; + if (subscriber != null) { // middle operator ... we pass thru unless a request has been made - if (toRequest == Long.MIN_VALUE) { + if (toRequest == NOT_SET) { // we pass-thru to the next producer as nothing has been requested - setProducer = true; + passToSubscriber = true; } - } } // do after releasing lock - if (setProducer) { - op.setProducer(p); + if (passToSubscriber) { + subscriber.setProducer(producer); } else { // we execute the request with whatever has been requested (or Long.MAX_VALUE) - if (toRequest == Long.MIN_VALUE) { - p.request(Long.MAX_VALUE); + if (toRequest == NOT_SET) { + producer.request(Long.MAX_VALUE); } else { - p.request(toRequest); + producer.request(toRequest); } } } From 0fbc4077df4ef73b1fb5ba5cee56ab17da47cc52 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 30 May 2015 12:30:55 +1000 Subject: [PATCH 286/857] include last value in cause when error occurs in takeUntil(predicate) --- .../operators/OperatorTakeUntilPredicate.java | 11 +++++++---- .../OperatorTakeUntilPredicateTest.java | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java index 5c3b2eae5c..668f049a99 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java @@ -18,6 +18,8 @@ import rx.Observable.Operator; import rx.*; import rx.annotations.Experimental; +import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; /** @@ -37,15 +39,16 @@ private ParentSubscriber(Subscriber child) { } @Override - public void onNext(T args) { - child.onNext(args); + public void onNext(T t) { + child.onNext(t); boolean stop = false; try { - stop = stopPredicate.call(args); + stop = stopPredicate.call(t); } catch (Throwable e) { done = true; - child.onError(e); + Exceptions.throwIfFatal(e); + child.onError(OnErrorThrowable.addValueAsLastCause(e, t)); unsubscribe(); return; } diff --git a/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java index f8e83f04ff..0bcf4757f7 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java @@ -16,6 +16,8 @@ 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.Mockito.*; @@ -132,4 +134,20 @@ public void onStart() { ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); Assert.assertEquals(0, ts.getOnCompletedEvents().size()); } + + @Test + public void testErrorIncludesLastValueAsCause() { + TestSubscriber ts = new TestSubscriber(); + final TestException e = new TestException("Forced failure"); + Observable.just("abc").takeUntil(new Func1() { + @Override + public Boolean call(String t) { + throw e; + } + }).subscribe(ts); + ts.assertTerminalEvent(); + ts.assertNotCompleted(); + assertEquals(1, (int) ts.getOnErrorEvents().size()); + assertTrue(ts.getOnErrorEvents().get(0).getCause().getMessage().contains("abc")); + } } From bd9926555dec028651a542a605275abb504c0a43 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 30 May 2015 13:01:01 +1000 Subject: [PATCH 287/857] fix render value in error last cause for primitive types --- .../java/rx/exceptions/OnErrorThrowable.java | 28 ++++++++++- .../java/rx/exceptions/OnNextValueTest.java | 49 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java index dc34843868..e54a9a80ce 100644 --- a/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -15,6 +15,9 @@ */ package rx.exceptions; +import java.util.HashSet; +import java.util.Set; + import rx.plugins.RxJavaErrorHandler; import rx.plugins.RxJavaPlugins; @@ -109,6 +112,27 @@ public static Throwable addValueAsLastCause(Throwable e, Object value) { public static class OnNextValue extends RuntimeException { private static final long serialVersionUID = -3454462756050397899L; + + // Lazy loaded singleton + private static final class Primitives { + + static final Set> INSTANCE = create(); + + private static Set> create() { + Set> set = new HashSet>(); + set.add(Boolean.class); + set.add(Character.class); + set.add(Byte.class); + set.add(Short.class); + set.add(Integer.class); + set.add(Long.class); + set.add(Float.class); + set.add(Double.class); + // Void is another primitive but cannot be instantiated + // and is caught by the null check in renderValue + return set; + } + } private final Object value; @@ -148,11 +172,11 @@ public Object getValue() { * @return a string version of the object if primitive or managed through error plugin, * otherwise the classname of the object */ - private static String renderValue(Object value){ + static String renderValue(Object value){ if (value == null) { return "null"; } - if (value.getClass().isPrimitive()) { + if (Primitives.INSTANCE.contains(value.getClass())) { return value.toString(); } if (value instanceof String) { diff --git a/src/test/java/rx/exceptions/OnNextValueTest.java b/src/test/java/rx/exceptions/OnNextValueTest.java index 2164aca595..b620e3eed0 100644 --- a/src/test/java/rx/exceptions/OnNextValueTest.java +++ b/src/test/java/rx/exceptions/OnNextValueTest.java @@ -15,14 +15,18 @@ */ package rx.exceptions; +import org.junit.Assert; import org.junit.Test; + import rx.Observable; import rx.Observer; +import rx.exceptions.OnErrorThrowable.OnNextValue; import rx.functions.Func1; import java.io.PrintWriter; import java.io.StringWriter; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -118,4 +122,49 @@ public BadToString call(BadToString badToString) { }).subscribe(observer); } + + @Test + public void testRenderInteger() { + assertEquals("123", OnNextValue.renderValue(123)); + } + + @Test + public void testRenderByte() { + assertEquals("10", OnNextValue.renderValue((byte) 10)); + } + + @Test + public void testRenderBoolean() { + assertEquals("true", OnNextValue.renderValue(true)); + } + + @Test + public void testRenderShort() { + assertEquals("10", OnNextValue.renderValue((short) 10)); + } + + @Test + public void testRenderLong() { + assertEquals("10", OnNextValue.renderValue(10L)); + } + + @Test + public void testRenderCharacter() { + assertEquals("10", OnNextValue.renderValue(10L)); + } + + @Test + public void testRenderFloat() { + assertEquals("10.0", OnNextValue.renderValue(10.0f)); + } + + @Test + public void testRenderDouble() { + assertEquals("10.0", OnNextValue.renderValue(10.0)); + } + + @Test + public void testRenderVoid() { + assertEquals("null", OnNextValue.renderValue((Void) null)); + } } From 2d98e3480074b5e44ee06bc254239807662121b3 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 30 May 2015 17:54:10 +1000 Subject: [PATCH 288/857] add value as last cause to error in takeWhile(predicate) --- .../internal/operators/OperatorTakeWhile.java | 11 +++++++---- .../operators/OperatorTakeWhileTest.java | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorTakeWhile.java b/src/main/java/rx/internal/operators/OperatorTakeWhile.java index 121ac0cc08..7d7a219270 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeWhile.java +++ b/src/main/java/rx/internal/operators/OperatorTakeWhile.java @@ -17,6 +17,8 @@ import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; import rx.functions.Func2; @@ -52,18 +54,19 @@ public Subscriber call(final Subscriber subscriber) { private boolean done = false; @Override - public void onNext(T args) { + public void onNext(T t) { boolean isSelected; try { - isSelected = predicate.call(args, counter++); + isSelected = predicate.call(t, counter++); } catch (Throwable e) { done = true; - subscriber.onError(e); + Exceptions.throwIfFatal(e); + subscriber.onError(OnErrorThrowable.addValueAsLastCause(e, t)); unsubscribe(); return; } if (isSelected) { - subscriber.onNext(args); + subscriber.onNext(t); } else { done = true; subscriber.onCompleted(); diff --git a/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java b/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java index 33e5b2c881..8317e41b65 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeWhileTest.java @@ -15,6 +15,7 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; @@ -25,6 +26,7 @@ import rx.*; import rx.Observable.OnSubscribe; +import rx.exceptions.TestException; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.subjects.*; @@ -261,4 +263,20 @@ public Boolean call(Integer t1) { Assert.assertFalse("Unsubscribed!", ts.isUnsubscribed()); } + + @Test + public void testErrorCauseIncludesLastValue() { + TestSubscriber ts = new TestSubscriber(); + Observable.just("abc").takeWhile(new Func1() { + @Override + public Boolean call(String t1) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertTerminalEvent(); + ts.assertNoValues(); + assertTrue(ts.getOnErrorEvents().get(0).getCause().getMessage().contains("abc")); + } + } From d32a1b0ea7ee455a23082cd1a7db80b3adef6d2b Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sun, 31 May 2015 17:17:29 +1000 Subject: [PATCH 289/857] add request overflow checking to OperatorSwitch --- .../rx/internal/operators/OperatorSwitch.java | 27 +++-- .../operators/OperatorSwitchTest.java | 98 ++++++++++++++++++- 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSwitch.java b/src/main/java/rx/internal/operators/OperatorSwitch.java index eae4d3aa67..afd35e477d 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitch.java +++ b/src/main/java/rx/internal/operators/OperatorSwitch.java @@ -94,15 +94,26 @@ public void request(long n) { synchronized (guard) { localSubscriber = currentSubscriber; if (currentSubscriber == null) { - initialRequested = n; + long r = initialRequested + n; + if (r < 0) { + infinite = true; + } else { + initialRequested = r; + } } else { - // If n == Long.MAX_VALUE, infinite will become true. Then currentSubscriber.requested won't be used. - // Therefore we don't need to worry about overflow. - currentSubscriber.requested += n; + long r = currentSubscriber.requested + n; + if (r < 0) { + infinite = true; + } else { + currentSubscriber.requested = r; + } } } if (localSubscriber != null) { - localSubscriber.requestMore(n); + if (infinite) + localSubscriber.requestMore(Long.MAX_VALUE); + else + localSubscriber.requestMore(n); } } }); @@ -167,7 +178,8 @@ void emit(T value, int id, InnerSubscriber innerSubscriber) { if (queue == null) { queue = new ArrayList(); } - innerSubscriber.requested--; + if (innerSubscriber.requested != Long.MAX_VALUE) + innerSubscriber.requested--; queue.add(value); return; } @@ -183,7 +195,8 @@ void emit(T value, int id, InnerSubscriber innerSubscriber) { if (once) { once = false; synchronized (guard) { - innerSubscriber.requested--; + if (innerSubscriber.requested != Long.MAX_VALUE) + innerSubscriber.requested--; } s.onNext(value); } diff --git a/src/test/java/rx/internal/operators/OperatorSwitchTest.java b/src/test/java/rx/internal/operators/OperatorSwitchTest.java index 0efc388db1..6b5d3a1f79 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchTest.java @@ -15,12 +15,20 @@ */ 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.*; +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.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -36,6 +44,7 @@ import rx.Subscriber; import rx.exceptions.TestException; import rx.functions.Action0; +import rx.functions.Action1; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; @@ -574,4 +583,91 @@ public void onNext(String t) { Assert.assertEquals(250, ts.getOnNextEvents().size()); } + + @Test(timeout = 10000) + public void testInitialRequestsAreAdditive() { + TestSubscriber ts = new TestSubscriber(0); + Observable.switchOnNext( + Observable.interval(100, TimeUnit.MILLISECONDS) + .map( + new Func1>() { + @Override + public Observable call(Long t) { + return Observable.just(1L, 2L, 3L); + } + } + ).take(3)) + .subscribe(ts); + ts.requestMore(Long.MAX_VALUE - 100); + ts.requestMore(1); + ts.awaitTerminalEvent(); + } + + @Test(timeout = 10000) + public void testInitialRequestsDontOverflow() { + TestSubscriber ts = new TestSubscriber(0); + Observable.switchOnNext( + Observable.interval(100, TimeUnit.MILLISECONDS) + .map(new Func1>() { + @Override + public Observable call(Long t) { + return Observable.from(Arrays.asList(1L, 2L, 3L)); + } + }).take(3)).subscribe(ts); + ts.requestMore(Long.MAX_VALUE - 1); + ts.requestMore(2); + ts.awaitTerminalEvent(); + assertTrue(ts.getOnNextEvents().size() > 0); + } + + + @Test(timeout = 10000) + public void testSecondaryRequestsDontOverflow() throws InterruptedException { + TestSubscriber ts = new TestSubscriber(0); + Observable.switchOnNext( + Observable.interval(100, TimeUnit.MILLISECONDS) + .map(new Func1>() { + @Override + public Observable call(Long t) { + return Observable.from(Arrays.asList(1L, 2L, 3L)); + } + }).take(3)).subscribe(ts); + ts.requestMore(1); + //we will miss two of the first observable + Thread.sleep(250); + ts.requestMore(Long.MAX_VALUE - 1); + ts.requestMore(Long.MAX_VALUE - 1); + ts.awaitTerminalEvent(); + ts.assertValueCount(7); + } + + @Test(timeout = 10000) + public void testSecondaryRequestsAdditivelyAreMoreThanLongMaxValueInducesMaxValueRequestFromUpstream() throws InterruptedException { + final List requests = new CopyOnWriteArrayList(); + final Action1 addRequest = new Action1() { + + @Override + public void call(Long n) { + requests.add(n); + }}; + TestSubscriber ts = new TestSubscriber(0); + Observable.switchOnNext( + Observable.interval(100, TimeUnit.MILLISECONDS) + .map(new Func1>() { + @Override + public Observable call(Long t) { + return Observable.from(Arrays.asList(1L, 2L, 3L)).doOnRequest(addRequest); + } + }).take(3)).subscribe(ts); + ts.requestMore(1); + //we will miss two of the first observable + Thread.sleep(250); + ts.requestMore(Long.MAX_VALUE - 1); + ts.requestMore(Long.MAX_VALUE - 1); + ts.awaitTerminalEvent(); + assertTrue(ts.getOnNextEvents().size() > 0); + assertEquals(5, (int) requests.size()); + assertEquals(Long.MAX_VALUE, (long) requests.get(3)); + assertEquals(Long.MAX_VALUE, (long) requests.get(4)); + } } From eadd43f007ff41b3cc73739ab58c4a7b7258aa39 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 1 Jun 2015 13:06:22 +1000 Subject: [PATCH 290/857] fix OnSubscribeRedo race conditions --- .../internal/operators/OnSubscribeRedo.java | 95 ++++++---- .../internal/operators/OperatorRetryTest.java | 170 +++++++++--------- 2 files changed, 147 insertions(+), 118 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 1ba5d1f281..1431d4581c 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -35,7 +35,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import rx.Notification; import rx.Observable; @@ -47,13 +46,15 @@ import rx.functions.Action0; import rx.functions.Func1; import rx.functions.Func2; +import rx.internal.producers.ProducerArbiter; +import rx.observers.Subscribers; import rx.schedulers.Schedulers; -import rx.subjects.PublishSubject; +import rx.subjects.BehaviorSubject; import rx.subscriptions.SerialSubscription; public final class OnSubscribeRedo implements OnSubscribe { - static final Func1>, Observable> REDO_INIFINITE = new Func1>, Observable>() { + static final Func1>, Observable> REDO_INFINITE = new Func1>, Observable>() { @Override public Observable call(Observable> ts) { return ts.map(new Func1, Notification>() { @@ -120,7 +121,7 @@ public Notification call(Notification n, Notification term) } public static Observable retry(Observable source) { - return retry(source, REDO_INIFINITE); + return retry(source, REDO_INFINITE); } public static Observable retry(Observable source, final long count) { @@ -144,7 +145,7 @@ public static Observable repeat(Observable source) { } public static Observable repeat(Observable source, Scheduler scheduler) { - return repeat(source, REDO_INIFINITE, scheduler); + return repeat(source, REDO_INFINITE, scheduler); } public static Observable repeat(Observable source, final long count) { @@ -172,10 +173,10 @@ public static Observable redo(Observable source, Func1(source, notificationHandler, false, false, scheduler)); } - private Observable source; + private final Observable source; private final Func1>, ? extends Observable> controlHandlerFunction; - private boolean stopOnComplete; - private boolean stopOnError; + private final boolean stopOnComplete; + private final boolean stopOnError; private final Scheduler scheduler; private OnSubscribeRedo(Observable source, Func1>, ? extends Observable> f, boolean stopOnComplete, boolean stopOnError, @@ -189,11 +190,12 @@ private OnSubscribeRedo(Observable source, Func1 child) { - final AtomicBoolean isLocked = new AtomicBoolean(true); + + // when true is a marker to say we are ready to resubscribe to source final AtomicBoolean resumeBoundary = new AtomicBoolean(true); + // incremented when requests are made, decremented when requests are fulfilled final AtomicLong consumerCapacity = new AtomicLong(0l); - final AtomicReference currentProducer = new AtomicReference(); final Scheduler.Worker worker = scheduler.createWorker(); child.add(worker); @@ -201,8 +203,18 @@ public void call(final Subscriber child) { final SerialSubscription sourceSubscriptions = new SerialSubscription(); child.add(sourceSubscriptions); - final PublishSubject> terminals = PublishSubject.create(); - + // use a subject to receive terminals (onCompleted and onError signals) from + // the source observable. We use a BehaviorSubject because subscribeToSource + // may emit a terminal before the restarts observable (transformed terminals) + // is subscribed + final BehaviorSubject> terminals = BehaviorSubject.create(); + final Subscriber> dummySubscriber = Subscribers.empty(); + // subscribe immediately so the last emission will be replayed to the next + // subscriber (which is the one we care about) + terminals.subscribe(dummySubscriber); + + final ProducerArbiter arbiter = new ProducerArbiter(); + final Action0 subscribeToSource = new Action0() { @Override public void call() { @@ -212,11 +224,11 @@ public void call() { Subscriber terminalDelegatingSubscriber = new Subscriber() { boolean done; + @Override public void onCompleted() { if (!done) { done = true; - currentProducer.set(null); unsubscribe(); terminals.onNext(Notification.createOnCompleted()); } @@ -226,7 +238,6 @@ public void onCompleted() { public void onError(Throwable e) { if (!done) { done = true; - currentProducer.set(null); unsubscribe(); terminals.onNext(Notification.createOnError(e)); } @@ -235,20 +246,30 @@ public void onError(Throwable e) { @Override public void onNext(T v) { if (!done) { - if (consumerCapacity.get() != Long.MAX_VALUE) { - consumerCapacity.decrementAndGet(); - } child.onNext(v); + decrementConsumerCapacity(); + arbiter.produced(1); + } + } + + private void decrementConsumerCapacity() { + // use a CAS loop because we don't want to decrement the + // value if it is Long.MAX_VALUE + while (true) { + long cc = consumerCapacity.get(); + if (cc != Long.MAX_VALUE) { + if (consumerCapacity.compareAndSet(cc, cc - 1)) { + break; + } + } else { + break; + } } } @Override public void setProducer(Producer producer) { - currentProducer.set(producer); - long c = consumerCapacity.get(); - if (c > 0) { - producer.request(c); - } + arbiter.setProducer(producer); } }; // new subscription each time so if it unsubscribes itself it does not prevent retries @@ -278,12 +299,11 @@ public void onError(Throwable e) { @Override public void onNext(Notification t) { - if (t.isOnCompleted() && stopOnComplete) - child.onCompleted(); - else if (t.isOnError() && stopOnError) - child.onError(t.getThrowable()); - else { - isLocked.set(false); + if (t.isOnCompleted() && stopOnComplete) { + filteredTerminals.onCompleted(); + } else if (t.isOnError() && stopOnError) { + filteredTerminals.onError(t.getThrowable()); + } else { filteredTerminals.onNext(t); } } @@ -313,10 +333,15 @@ public void onError(Throwable e) { @Override public void onNext(Object t) { - if (!isLocked.get() && !child.isUnsubscribed()) { + if (!child.isUnsubscribed()) { + // perform a best endeavours check on consumerCapacity + // with the intent of only resubscribing immediately + // if there is outstanding capacity if (consumerCapacity.get() > 0) { worker.schedule(subscribeToSource); } else { + // set this to true so that on next request + // subscribeToSource will be scheduled resumeBoundary.compareAndSet(false, true); } } @@ -334,13 +359,11 @@ public void setProducer(Producer producer) { @Override public void request(final long n) { - long c = BackpressureUtils.getAndAddRequest(consumerCapacity, n); - Producer producer = currentProducer.get(); - if (producer != null) { - producer.request(n); - } else - if (c == 0 && resumeBoundary.compareAndSet(true, false)) { - worker.schedule(subscribeToSource); + if (n > 0) { + BackpressureUtils.getAndAddRequest(consumerCapacity, n); + arbiter.request(n); + if (resumeBoundary.compareAndSet(true, false)) + worker.schedule(subscribeToSource); } } }); diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index a5aa9f1c31..146ee3c254 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -395,9 +395,13 @@ public static class FuncWithErrors implements Observable.OnSubscribe { public void call(final Subscriber o) { o.setProducer(new Producer() { final AtomicLong req = new AtomicLong(); + // 0 = not set, 1 = fast path, 2 = backpressure + final AtomicInteger path = new AtomicInteger(0); + volatile boolean done = false; + @Override public void request(long n) { - if (n == Long.MAX_VALUE) { + if (n == Long.MAX_VALUE && path.compareAndSet(0, 1)) { o.onNext("beginningEveryTime"); int i = count.getAndIncrement(); if (i < numFailures) { @@ -408,11 +412,12 @@ public void request(long n) { } return; } - if (n > 0 && req.getAndAdd(n) == 0) { + if (n > 0 && req.getAndAdd(n) == 0 && (path.get() == 2 || path.compareAndSet(0, 2)) && !done) { int i = count.getAndIncrement(); if (i < numFailures) { o.onNext("beginningEveryTime"); o.onError(new RuntimeException("forced failure: " + (i + 1))); + done = true; } else { do { if (i == numFailures) { @@ -421,6 +426,7 @@ public void request(long n) { if (i > numFailures) { o.onNext("onSuccessOnly"); o.onCompleted(); + done = true; break; } i = count.getAndIncrement(); @@ -682,91 +688,88 @@ public void testTimeoutWithRetry() { assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); } - @Test(timeout = 15000) + @Test//(timeout = 15000) public void testRetryWithBackpressure() throws InterruptedException { - final int NUM_RETRIES = RxRingBuffer.SIZE * 2; - for (int i = 0; i < 400; i++) { - @SuppressWarnings("unchecked") - Observer observer = mock(Observer.class); - Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); - TestSubscriber ts = new TestSubscriber(observer); - origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); - ts.awaitTerminalEvent(5, TimeUnit.SECONDS); - - InOrder inOrder = inOrder(observer); - // should have no errors - verify(observer, never()).onError(any(Throwable.class)); - // should show NUM_RETRIES attempts - inOrder.verify(observer, times(NUM_RETRIES + 1)).onNext("beginningEveryTime"); - // should have a single success - inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); - // should have a single successful onCompleted - inOrder.verify(observer, times(1)).onCompleted(); - inOrder.verifyNoMoreInteractions(); + final int NUM_LOOPS = 1; + for (int j=0;j observer = mock(Observer.class); + Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); + TestSubscriber ts = new TestSubscriber(observer); + origin.retry().observeOn(Schedulers.computation()).unsafeSubscribe(ts); + ts.awaitTerminalEvent(5, TimeUnit.SECONDS); + + InOrder inOrder = inOrder(observer); + // should have no errors + verify(observer, never()).onError(any(Throwable.class)); + // should show NUM_RETRIES attempts + inOrder.verify(observer, times(NUM_RETRIES + 1)).onNext("beginningEveryTime"); + // should have a single success + inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onCompleted + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } } } - @Test(timeout = 15000) + @Test//(timeout = 15000) public void testRetryWithBackpressureParallel() throws InterruptedException { + final int NUM_LOOPS = 1; final int NUM_RETRIES = RxRingBuffer.SIZE * 2; int ncpu = Runtime.getRuntime().availableProcessors(); ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 2)); - final AtomicInteger timeouts = new AtomicInteger(); - final Map> data = new ConcurrentHashMap>(); - final Map> exceptions = new ConcurrentHashMap>(); - final Map completions = new ConcurrentHashMap(); - - int m = 5000; - final CountDownLatch cdl = new CountDownLatch(m); - for (int i = 0; i < m; i++) { - final int j = i; - exec.execute(new Runnable() { - @Override - public void run() { - final AtomicInteger nexts = new AtomicInteger(); - try { - Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); - TestSubscriber ts = new TestSubscriber(); - origin.retry() - .observeOn(Schedulers.computation()).unsafeSubscribe(ts); - ts.awaitTerminalEvent(2500, TimeUnit.MILLISECONDS); - if (ts.getOnCompletedEvents().size() != 1) { - completions.put(j, ts.getOnCompletedEvents().size()); - } - if (ts.getOnErrorEvents().size() != 0) { - exceptions.put(j, ts.getOnErrorEvents()); - } - if (ts.getOnNextEvents().size() != NUM_RETRIES + 2) { - data.put(j, ts.getOnNextEvents()); + try { + for (int r = 0; r < NUM_LOOPS; r++) { + if (r % 10 == 0) { + System.out.println("testRetryWithBackpressureParallelLoop -> " + r); + } + + final AtomicInteger timeouts = new AtomicInteger(); + final Map> data = new ConcurrentHashMap>(); + + int m = 5000; + final CountDownLatch cdl = new CountDownLatch(m); + for (int i = 0; i < m; i++) { + final int j = i; + exec.execute(new Runnable() { + @Override + public void run() { + final AtomicInteger nexts = new AtomicInteger(); + try { + Observable origin = Observable.create(new FuncWithErrors(NUM_RETRIES)); + TestSubscriber ts = new TestSubscriber(); + origin.retry() + .observeOn(Schedulers.computation()).unsafeSubscribe(ts); + ts.awaitTerminalEvent(2500, TimeUnit.MILLISECONDS); + List onNextEvents = new ArrayList(ts.getOnNextEvents()); + if (onNextEvents.size() != NUM_RETRIES + 2) { + for (Throwable t : ts.getOnErrorEvents()) { + onNextEvents.add(t.toString()); + } + for (Object o : ts.getOnCompletedEvents()) { + onNextEvents.add("onCompleted"); + } + data.put(j, onNextEvents); + } + } catch (Throwable t) { + timeouts.incrementAndGet(); + System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); + } + cdl.countDown(); } - } catch (Throwable t) { - timeouts.incrementAndGet(); - System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); - } - cdl.countDown(); + }); } - }); - } - exec.shutdown(); - cdl.await(); - assertEquals(0, timeouts.get()); - if (data.size() > 0) { - System.out.println(allSequenceFrequency(data)); - } - if (exceptions.size() > 0) { - System.out.println(exceptions); - } - if (completions.size() > 0) { - System.out.println(completions); - } - if (data.size() > 0) { - fail("Data content mismatch: " + allSequenceFrequency(data)); - } - if (exceptions.size() > 0) { - fail("Exceptions received: " + exceptions); - } - if (completions.size() > 0) { - fail("Multiple completions received: " + completions); + cdl.await(); + assertEquals(0, timeouts.get()); + if (data.size() > 0) { + fail("Data content mismatch: " + allSequenceFrequency(data)); + } + } + } finally { + exec.shutdown(); } } static StringBuilder allSequenceFrequency(Map> its) { @@ -783,10 +786,10 @@ static StringBuilder allSequenceFrequency(Map> its) { } static StringBuilder sequenceFrequency(Iterable it) { StringBuilder sb = new StringBuilder(); - + Object prev = null; int cnt = 0; - + for (Object curr : it) { if (sb.length() > 0) { if (!curr.equals(prev)) { @@ -805,10 +808,13 @@ static StringBuilder sequenceFrequency(Iterable it) { } prev = curr; } - + if (cnt > 1) { + sb.append(" x ").append(cnt); + } + return sb; } - @Test(timeout = 3000) + @Test//(timeout = 3000) public void testIssue1900() throws InterruptedException { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); @@ -849,7 +855,7 @@ public Observable call(GroupedObservable t1) { inOrder.verify(observer, times(1)).onCompleted(); inOrder.verifyNoMoreInteractions(); } - @Test(timeout = 3000) + @Test//(timeout = 3000) public void testIssue1900SourceNotSupportingBackpressure() { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); From c4fcd918779e5fad668a48a54ffa7354795e1adc Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sun, 31 May 2015 19:10:20 +1000 Subject: [PATCH 291/857] fix request processing in OperatorSwitchIfNext --- .../operators/OperatorSwitchIfEmpty.java | 101 +++++++++--------- .../operators/OperatorSwitchIfEmptyTest.java | 53 ++++++++- 2 files changed, 101 insertions(+), 53 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java index 4b106f9be5..e14d9ace16 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java +++ b/src/main/java/rx/internal/operators/OperatorSwitchIfEmpty.java @@ -15,9 +15,9 @@ */ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicLong; import rx.*; +import rx.internal.producers.ProducerArbiter; import rx.subscriptions.SerialSubscription; /** @@ -35,36 +35,32 @@ public OperatorSwitchIfEmpty(Observable alternate) { @Override public Subscriber call(Subscriber child) { final SerialSubscription ssub = new SerialSubscription(); - final SwitchIfEmptySubscriber parent = new SwitchIfEmptySubscriber(child, ssub); + ProducerArbiter arbiter = new ProducerArbiter(); + final ParentSubscriber parent = new ParentSubscriber(child, ssub, arbiter, alternate); ssub.set(parent); child.add(ssub); + child.setProducer(arbiter); return parent; } - private class SwitchIfEmptySubscriber extends Subscriber { - - boolean empty = true; - final AtomicLong consumerCapacity = new AtomicLong(0l); + private static final class ParentSubscriber extends Subscriber { + private boolean empty = true; private final Subscriber child; - final SerialSubscription ssub; + private final SerialSubscription ssub; + private final ProducerArbiter arbiter; + private final Observable alternate; - public SwitchIfEmptySubscriber(Subscriber child, final SerialSubscription ssub) { + ParentSubscriber(Subscriber child, final SerialSubscription ssub, ProducerArbiter arbiter, Observable alternate) { this.child = child; this.ssub = ssub; + this.arbiter = arbiter; + this.alternate = alternate; } @Override public void setProducer(final Producer producer) { - super.setProducer(new Producer() { - @Override - public void request(long n) { - if (empty) { - consumerCapacity.set(n); - } - producer.request(n); - } - }); + arbiter.setProducer(producer); } @Override @@ -77,41 +73,9 @@ public void onCompleted() { } private void subscribeToAlternate() { - ssub.set(alternate.unsafeSubscribe(new Subscriber() { - - @Override - public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - @Override - public void request(long n) { - producer.request(n); - } - }); - } - - @Override - public void onStart() { - final long capacity = consumerCapacity.get(); - if (capacity > 0) { - request(capacity); - } - } - - @Override - public void onCompleted() { - child.onCompleted(); - } - - @Override - public void onError(Throwable e) { - child.onError(e); - } - - @Override - public void onNext(T t) { - child.onNext(t); - } - })); + AlternateSubscriber as = new AlternateSubscriber(child, arbiter); + ssub.set(as); + alternate.unsafeSubscribe(as); } @Override @@ -123,6 +87,39 @@ public void onError(Throwable e) { public void onNext(T t) { empty = false; child.onNext(t); + arbiter.produced(1); + } + } + + private static final class AlternateSubscriber extends Subscriber { + + private final ProducerArbiter arbiter; + private final Subscriber child; + + AlternateSubscriber(Subscriber child, ProducerArbiter arbiter) { + this.child = child; + this.arbiter = arbiter; + } + + @Override + public void setProducer(final Producer producer) { + arbiter.setProducer(producer); + } + + @Override + public void onCompleted() { + child.onCompleted(); } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + child.onNext(t); + arbiter.produced(1); + } } } diff --git a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java index ce52bccd6f..2534613ab4 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java @@ -18,14 +18,18 @@ import static org.junit.Assert.*; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import rx.*; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.functions.Action0; +import rx.functions.Action1; import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; public class OperatorSwitchIfEmptyTest { @@ -142,6 +146,10 @@ public void onStart() { assertEquals(Arrays.asList(1), ts.getOnNextEvents()); ts.assertNoErrors(); + ts.requestMore(1); + ts.assertValueCount(2); + ts.requestMore(1); + ts.assertValueCount(3); } @Test public void testBackpressureNoRequest() { @@ -153,8 +161,51 @@ public void onStart() { } }; Observable.empty().switchIfEmpty(Observable.just(1, 2, 3)).subscribe(ts); - assertTrue(ts.getOnNextEvents().isEmpty()); ts.assertNoErrors(); } + + @Test + public void testBackpressureOnFirstObservable() { + TestSubscriber ts = new TestSubscriber(0); + Observable.just(1,2,3).switchIfEmpty(Observable.just(4, 5, 6)).subscribe(ts); + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertNoValues(); + } + + @Test(timeout = 10000) + public void testRequestsNotLost() throws InterruptedException { + final TestSubscriber ts = new TestSubscriber(0); + Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber subscriber) { + subscriber.setProducer(new Producer() { + final AtomicBoolean completed = new AtomicBoolean(false); + @Override + public void request(long n) { + if (n > 0 && completed.compareAndSet(false, true)) { + Schedulers.io().createWorker().schedule(new Action0() { + @Override + public void call() { + subscriber.onCompleted(); + }}, 100, TimeUnit.MILLISECONDS); + } + }}); + }}) + .switchIfEmpty(Observable.from(Arrays.asList(1L, 2L, 3L))) + .subscribeOn(Schedulers.computation()) + .subscribe(ts); + ts.requestMore(0); + Thread.sleep(50); + //request while first observable is still finishing (as empty) + ts.requestMore(1); + ts.requestMore(1); + Thread.sleep(500); + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertValueCount(2); + ts.unsubscribe(); + } } \ No newline at end of file From a90788c2d564c025108de4ca790a7c42d9dc7682 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Tue, 2 Jun 2015 16:40:39 +0800 Subject: [PATCH 292/857] Fix a wrong assertion in assertError --- src/main/java/rx/observers/TestSubscriber.java | 2 +- src/test/java/rx/observers/TestSubscriberTest.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 56d12b46e2..bdb69663a3 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -380,7 +380,7 @@ public void assertError(Throwable throwable) { if (err.size() > 1) { throw new AssertionError("Multiple errors: " + err.size(), new CompositeException(err)); } else - if (throwable.equals(err.get(0))) { + if (!throwable.equals(err.get(0))) { throw new AssertionError("Exceptions differ; expected: " + throwable + ", actual: " + err.get(0), err.get(0)); } } diff --git a/src/test/java/rx/observers/TestSubscriberTest.java b/src/test/java/rx/observers/TestSubscriberTest.java index c728e79cda..c07c261f77 100644 --- a/src/test/java/rx/observers/TestSubscriberTest.java +++ b/src/test/java/rx/observers/TestSubscriberTest.java @@ -121,4 +121,11 @@ public void testWrappingMockWhenUnsubscribeInvolved() { inOrder.verifyNoMoreInteractions(); } + @Test + public void testAssertError() { + RuntimeException e = new RuntimeException("Oops"); + TestSubscriber subscriber = new TestSubscriber(); + Observable.error(e).subscribe(subscriber); + subscriber.assertError(e); + } } From a22cce7c5e0da403849d265753c5dc1f889fc109 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Tue, 2 Jun 2015 17:12:27 +0800 Subject: [PATCH 293/857] Replace the Java 7 AssertionError(message, cause) with RuntimeException --- src/main/java/rx/observers/TestSubscriber.java | 18 ++++++++++++------ src/main/java/rx/subjects/ReplaySubject.java | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index bdb69663a3..c35475cfa9 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -356,10 +356,12 @@ public void assertError(Class clazz) { throw new AssertionError("No errors"); } else if (err.size() > 1) { - throw new AssertionError("Multiple errors: " + err.size(), new CompositeException(err)); + // can't use AssertionError because (message, cause) doesn't exist until Java 7 + throw new RuntimeException("Multiple errors: " + err.size(), new CompositeException(err)); } else if (!clazz.isInstance(err.get(0))) { - throw new AssertionError("Exceptions differ; expected: " + clazz + ", actual: " + err.get(0), err.get(0)); + // can't use AssertionError because (message, cause) doesn't exist until Java 7 + throw new RuntimeException("Exceptions differ; expected: " + clazz + ", actual: " + err.get(0), err.get(0)); } } @@ -378,10 +380,12 @@ public void assertError(Throwable throwable) { throw new AssertionError("No errors"); } else if (err.size() > 1) { - throw new AssertionError("Multiple errors: " + err.size(), new CompositeException(err)); + // can't use AssertionError because (message, cause) doesn't exist until Java 7 + throw new RuntimeException("Multiple errors: " + err.size(), new CompositeException(err)); } else if (!throwable.equals(err.get(0))) { - throw new AssertionError("Exceptions differ; expected: " + throwable + ", actual: " + err.get(0), err.get(0)); + // can't use AssertionError because (message, cause) doesn't exist until Java 7 + throw new RuntimeException("Exceptions differ; expected: " + throwable + ", actual: " + err.get(0), err.get(0)); } } @@ -400,9 +404,11 @@ public void assertNoTerminalEvent() { throw new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none"); } else if (err.size() == 1) { - throw new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none", err.get(0)); + // can't use AssertionError because (message, cause) doesn't exist until Java 7 + throw new RuntimeException("Found " + err.size() + " errors and " + s + " completion events instead of none", err.get(0)); } else { - throw new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none", new CompositeException(err)); + // can't use AssertionError because (message, cause) doesn't exist until Java 7 + throw new RuntimeException("Found " + err.size() + " errors and " + s + " completion events instead of none", new CompositeException(err)); } } } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index 418a7d7f4e..c3779dac2d 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -114,7 +114,7 @@ public void call(SubjectObserver o) { boolean skipFinal = false; try { for (;;) { - int idx = o.index(); + int idx = o.index(); int sidx = state.index; if (idx != sidx) { Integer j = state.replayObserverFromIndex(idx, o); From d16a62267ff6e6e26747758f15040ce9655aef36 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 2 Jun 2015 21:25:35 +1000 Subject: [PATCH 294/857] use Subscribers.from() --- .../operators/OperatorDelayWithSelector.java | 20 ++----------------- .../internal/operators/OperatorMulticast.java | 18 ++--------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java b/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java index dd90ec5e43..16744563d7 100644 --- a/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java +++ b/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java @@ -20,6 +20,7 @@ import rx.Subscriber; import rx.functions.Func1; import rx.observers.SerializedSubscriber; +import rx.observers.Subscribers; import rx.subjects.PublishSubject; /** @@ -44,24 +45,7 @@ public Subscriber call(final Subscriber _child) { final SerializedSubscriber child = new SerializedSubscriber(_child); final PublishSubject> delayedEmissions = PublishSubject.create(); - _child.add(Observable.merge(delayedEmissions).unsafeSubscribe(new Subscriber() { - - @Override - public void onCompleted() { - child.onCompleted(); - } - - @Override - public void onError(Throwable e) { - child.onError(e); - } - - @Override - public void onNext(T t) { - child.onNext(t); - } - - })); + _child.add(Observable.merge(delayedEmissions).unsafeSubscribe(Subscribers.from(child))); return new Subscriber(_child) { diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java index 4d5d10f4f3..20e64d1ba4 100644 --- a/src/main/java/rx/internal/operators/OperatorMulticast.java +++ b/src/main/java/rx/internal/operators/OperatorMulticast.java @@ -26,6 +26,7 @@ import rx.functions.Action1; import rx.functions.Func0; import rx.observables.ConnectableObservable; +import rx.observers.Subscribers; import rx.subjects.Subject; import rx.subscriptions.Subscriptions; @@ -90,22 +91,7 @@ public void connect(Action1 connection) { final Subject subject = subjectFactory.call(); // create new Subscriber that will pass-thru to the subject we just created // we do this since it is also a Subscription whereas the Subject is not - subscription = new Subscriber() { - @Override - public void onCompleted() { - subject.onCompleted(); - } - - @Override - public void onError(Throwable e) { - subject.onError(e); - } - - @Override - public void onNext(T args) { - subject.onNext(args); - } - }; + subscription = Subscribers.from(subject); final AtomicReference gs = new AtomicReference(); gs.set(Subscriptions.create(new Action0() { @Override From 596ecd2a53f119086d406da21f0190a257437743 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Tue, 2 Jun 2015 20:10:21 +0800 Subject: [PATCH 295/857] Use initCause to initialize AssertionError --- .../java/rx/observers/TestSubscriber.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index c35475cfa9..284002d452 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -356,12 +356,14 @@ public void assertError(Class clazz) { throw new AssertionError("No errors"); } else if (err.size() > 1) { - // can't use AssertionError because (message, cause) doesn't exist until Java 7 - throw new RuntimeException("Multiple errors: " + err.size(), new CompositeException(err)); + AssertionError ae = new AssertionError("Multiple errors: " + err.size()); + ae.initCause(new CompositeException(err)); + throw ae; } else if (!clazz.isInstance(err.get(0))) { - // can't use AssertionError because (message, cause) doesn't exist until Java 7 - throw new RuntimeException("Exceptions differ; expected: " + clazz + ", actual: " + err.get(0), err.get(0)); + AssertionError ae = new AssertionError("Exceptions differ; expected: " + clazz + ", actual: " + err.get(0)); + ae.initCause(err.get(0)); + throw ae; } } @@ -380,12 +382,14 @@ public void assertError(Throwable throwable) { throw new AssertionError("No errors"); } else if (err.size() > 1) { - // can't use AssertionError because (message, cause) doesn't exist until Java 7 - throw new RuntimeException("Multiple errors: " + err.size(), new CompositeException(err)); + AssertionError ae = new AssertionError("Multiple errors: " + err.size()); + ae.initCause(new CompositeException(err)); + throw ae; } else if (!throwable.equals(err.get(0))) { - // can't use AssertionError because (message, cause) doesn't exist until Java 7 - throw new RuntimeException("Exceptions differ; expected: " + throwable + ", actual: " + err.get(0), err.get(0)); + AssertionError ae = new AssertionError("Exceptions differ; expected: " + throwable + ", actual: " + err.get(0)); + ae.initCause(err.get(0)); + throw ae; } } @@ -404,11 +408,13 @@ public void assertNoTerminalEvent() { throw new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none"); } else if (err.size() == 1) { - // can't use AssertionError because (message, cause) doesn't exist until Java 7 - throw new RuntimeException("Found " + err.size() + " errors and " + s + " completion events instead of none", err.get(0)); + AssertionError ae = new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none"); + ae.initCause(err.get(0)); + throw ae; } else { - // can't use AssertionError because (message, cause) doesn't exist until Java 7 - throw new RuntimeException("Found " + err.size() + " errors and " + s + " completion events instead of none", new CompositeException(err)); + AssertionError ae = new AssertionError("Found " + err.size() + " errors and " + s + " completion events instead of none"); + ae.initCause(new CompositeException(err)); + throw ae; } } } From 23c06b1d622c99edace6fed2325de5b574fd18c7 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 8 Jun 2015 14:28:12 +1000 Subject: [PATCH 296/857] add two unit tests for issue #3008 --- .../OperatorRetryWithPredicateTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java index ee4750829a..76461e3ddf 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java @@ -20,7 +20,10 @@ import static org.mockito.Mockito.*; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.*; @@ -305,4 +308,56 @@ public Integer call(Integer t1) { assertEquals(1, value); } + + @Test + public void testIssue3008RetryWithPredicate() { + final List list = new CopyOnWriteArrayList(); + final AtomicBoolean isFirst = new AtomicBoolean(true); + Observable. just(1L, 2L, 3L).map(new Func1(){ + @Override + public Long call(Long x) { + System.out.println("map " + x); + if (x == 2 && isFirst.getAndSet(false)) { + throw new RuntimeException("retryable error"); + } + return x; + }}) + .retry(new Func2() { + @Override + public Boolean call(Integer t1, Throwable t2) { + return true; + }}) + .forEach(new Action1() { + + @Override + public void call(Long t) { + System.out.println(t); + list.add(t); + }}); + assertEquals(Arrays.asList(1L,1L,2L,3L), list); + } + + @Test + public void testIssue3008RetryInfinite() { + final List list = new CopyOnWriteArrayList(); + final AtomicBoolean isFirst = new AtomicBoolean(true); + Observable. just(1L, 2L, 3L).map(new Func1(){ + @Override + public Long call(Long x) { + System.out.println("map " + x); + if (x == 2 && isFirst.getAndSet(false)) { + throw new RuntimeException("retryable error"); + } + return x; + }}) + .retry() + .forEach(new Action1() { + + @Override + public void call(Long t) { + System.out.println(t); + list.add(t); + }}); + assertEquals(Arrays.asList(1L,1L,2L,3L), list); + } } From 9fa9cea2a42a5ad3d779250846b6048f1b01d3bf Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 8 Jun 2015 16:22:27 +1000 Subject: [PATCH 297/857] ensure iterator.hasNext is not called unnecessarily as per #3006 --- .../operators/OnSubscribeFromIterable.java | 44 +++++---- .../OnSubscribeFromIterableTest.java | 93 +++++++++++++++++++ 2 files changed, 120 insertions(+), 17 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index 766b624416..2aad771b57 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -71,14 +71,19 @@ public void request(long n) { } if (n == Long.MAX_VALUE && REQUESTED_UPDATER.compareAndSet(this, 0, Long.MAX_VALUE)) { // fast-path without backpressure - while (it.hasNext()) { + + while (true) { if (o.isUnsubscribed()) { return; + } else if (it.hasNext()) { + o.onNext(it.next()); + } else if (!o.isUnsubscribed()) { + o.onCompleted(); + return; + } else { + // is unsubscribed + return; } - o.onNext(it.next()); - } - if (!o.isUnsubscribed()) { - o.onCompleted(); } } else if (n > 0) { // backpressure is requested @@ -86,27 +91,32 @@ public void request(long n) { if (_c == 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. + * 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 r = requested; long numToEmit = r; - while (it.hasNext() && --numToEmit >= 0) { + while (true) { if (o.isUnsubscribed()) { return; - } - o.onNext(it.next()); - - } - - if (!it.hasNext()) { - if (!o.isUnsubscribed()) { + } else if (it.hasNext()) { + if (--numToEmit >= 0) { + o.onNext(it.next()); + } else + break; + } else if (!o.isUnsubscribed()) { o.onCompleted(); + return; + } else { + // is unsubscribed + return; } - return; } if (REQUESTED_UPDATER.addAndGet(this, -r) == 0) { - // we're done emitting the number requested so return + // we're done emitting the number requested so + // return return; } diff --git a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java index 91bf65bf4d..a75e733951 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeFromIterableTest.java @@ -15,6 +15,7 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; @@ -29,6 +30,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -221,4 +223,95 @@ public void onNext(Object t) { assertTrue(completed.get()); } + @Test + public void testDoesNotCallIteratorHasNextMoreThanRequiredWithBackpressure() { + final AtomicBoolean called = new AtomicBoolean(false); + Iterable iterable = new Iterable() { + + @Override + public Iterator iterator() { + return new Iterator() { + + int count = 1; + + @Override + public void remove() { + // ignore + } + + @Override + public boolean hasNext() { + if (count > 1) { + called.set(true); + return false; + } else + return true; + } + + @Override + public Integer next() { + return count++; + } + + }; + } + }; + Observable.from(iterable).take(1).subscribe(); + assertFalse(called.get()); + } + + @Test + public void testDoesNotCallIteratorHasNextMoreThanRequiredFastPath() { + final AtomicBoolean called = new AtomicBoolean(false); + Iterable iterable = new Iterable() { + + @Override + public Iterator iterator() { + return new Iterator() { + + @Override + public void remove() { + // ignore + } + + int count = 1; + + @Override + public boolean hasNext() { + if (count > 1) { + called.set(true); + return false; + } else + return true; + } + + @Override + public Integer next() { + return count++; + } + + }; + } + }; + Observable.from(iterable).subscribe(new Subscriber() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + // unsubscribe on first emission + unsubscribe(); + } + }); + assertFalse(called.get()); + } + } From f9562938f2cb3f64fe21786b02338aff3311806b Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 9 Jun 2015 13:18:07 -0700 Subject: [PATCH 298/857] 1.0.12 --- CHANGES.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 810b586189..a92a299b5e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,24 @@ # RxJava Releases # +### Version 1.0.12 – June 9th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.12%7C)) ### + +* [Pull 2963] (https://github.com/ReactiveX/RxJava/pull/2963) Set of standard producers and updated queue implementations +* [Pull 2961] (https://github.com/ReactiveX/RxJava/pull/2961) fix Amb backpressure bug +* [Pull 2960] (https://github.com/ReactiveX/RxJava/pull/2960) fix OperatorConcat race condition where request lost +* [Pull 2985] (https://github.com/ReactiveX/RxJava/pull/2985) improve OperatorSerializeTest.testMultiThreadedWithNPEinMiddle +* [Pull 2986] (https://github.com/ReactiveX/RxJava/pull/2986) OperatorAll - implement backpressure and include last value in exception cause +* [Pull 2987] (https://github.com/ReactiveX/RxJava/pull/2987) fix skip() race condition and request overflow +* [Pull 2988] (https://github.com/ReactiveX/RxJava/pull/2988) Operator exists() - implement backpressure & include last value in exception cause +* [Pull 2989] (https://github.com/ReactiveX/RxJava/pull/2989) prevent take() from requesting more than needed +* [Pull 2991] (https://github.com/ReactiveX/RxJava/pull/2991) takeUntil(predicate) - include last value in error cause +* [Pull 2992] (https://github.com/ReactiveX/RxJava/pull/2992) Fix value rendering in error last cause for primitive types +* [Pull 2993] (https://github.com/ReactiveX/RxJava/pull/2993) takeWhile(predicate) - include last value in error cause +* [Pull 2996] (https://github.com/ReactiveX/RxJava/pull/2996) switchIfEmpty - fix backpressure bug and lost requests +* [Pull 2999] (https://github.com/ReactiveX/RxJava/pull/2999) Fix a wrong assertion in assertError +* [Pull 3000] (https://github.com/ReactiveX/RxJava/pull/3000) Replace the Java 7 AssertionError(message, cause) with initCause +* [Pull 3001] (https://github.com/ReactiveX/RxJava/pull/3001) use Subscribers.from() +* [Pull 3009] (https://github.com/ReactiveX/RxJava/pull/3009) Observable.from(iterable) - ensure it.hasNext() is not called unnecessarily + ### Version 1.0.11 – May 19th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.11%7C)) ### * [Pull 2948] (https://github.com/ReactiveX/RxJava/pull/2948) More assertions for TestSubscriber From 0e0949d8458d3cbe48ecb2d62a0234697c475020 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Sat, 7 Feb 2015 21:30:52 -0800 Subject: [PATCH 299/857] rx.Single Adds `rx.Single` as an "Observable Future" for representing work with a single return value. See https://github.com/ReactiveX/RxJava/issues/1594 rx.Future/Task/Async/Single This provides a type similar to `Future` in that it represents a scalar unit of work, but it is lazy like an `Observable` and many `Single`s can be combined into an `Observable` stream. Note how `Single.zip` returns `Single` whereas `Single.merge` returns `Observable`. Examples of using this class: ```java import rx.Observable; import rx.Single; public class TaskExamples { public static void main(String... args) { // scalar synchronous value Single t1 = Single.create(t -> { t.onSuccess("Hello World!"); }); // scalar synchronous value using helper method Single t2 = Single.just(1); // synchronous error Single error = Single.create(t -> { t.onError(new RuntimeException("failed!")); }); // executing t1.subscribe(System.out::println); t2.subscribe(System.out::println); error.subscribe(System.out::println, e -> System.out.println(e.getMessage())); // scalar Singles for request/response like a Future getData(1).subscribe(System.out::println); // combining Tasks into another Task Single zipped = Single.zip(t1, t2, (a, b) -> a + " -- " + b); // combining Singles into an Observable stream Observable merged = Single.merge(t1, t2.map(String::valueOf), getData(3)); Observable mergeWith = t1.mergeWith(t2.map(String::valueOf)); zipped.subscribe(v -> System.out.println("zipped => " + v)); merged.subscribe(v -> System.out.println("merged => " + v)); mergeWith.subscribe(v -> System.out.println("mergeWith => " + v)); } /** * Example of an async scalar execution using Single.create *

* This shows the lazy, idiomatic approach for Rx exactly like an Observable except scalar. * * @param arg * @return */ public static Single getData(int arg) { return Single.create(s -> { new Thread(() -> { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } // deliver value s.onSuccess("Data=" + arg); }).start(); }); } } ``` --- src/main/java/rx/Single.java | 1858 +++++++++++++++++ src/main/java/rx/SingleSubscriber.java | 81 + ...eline.java => ObservablePerfBaseline.java} | 2 +- src/perf/java/rx/SinglePerfBaseline.java | 100 + src/test/java/rx/SingleTest.java | 455 ++++ 5 files changed, 2495 insertions(+), 1 deletion(-) create mode 100644 src/main/java/rx/Single.java create mode 100644 src/main/java/rx/SingleSubscriber.java rename src/perf/java/rx/{PerfBaseline.java => ObservablePerfBaseline.java} (98%) create mode 100644 src/perf/java/rx/SinglePerfBaseline.java create mode 100644 src/test/java/rx/SingleTest.java diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java new file mode 100644 index 0000000000..d8fcf88b87 --- /dev/null +++ b/src/main/java/rx/Single.java @@ -0,0 +1,1858 @@ +/** + * Copyright 2015 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 java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import rx.Observable.Operator; +import rx.annotations.Experimental; +import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorNotImplementedException; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.functions.Func3; +import rx.functions.Func4; +import rx.functions.Func5; +import rx.functions.Func6; +import rx.functions.Func7; +import rx.functions.Func8; +import rx.functions.Func9; +import rx.internal.operators.OnSubscribeToObservableFuture; +import rx.internal.operators.OperatorMap; +import rx.internal.operators.OperatorObserveOn; +import rx.internal.operators.OperatorOnErrorReturn; +import rx.internal.operators.OperatorSubscribeOn; +import rx.internal.operators.OperatorTimeout; +import rx.internal.operators.OperatorZip; +import rx.internal.producers.SingleDelayedProducer; +import rx.observers.SafeSubscriber; +import rx.plugins.RxJavaObservableExecutionHook; +import rx.plugins.RxJavaPlugins; +import rx.schedulers.Schedulers; +import rx.subscriptions.Subscriptions; + +/** + * The Single class that implements the Reactive Pattern for a single value response. See {@link Observable} for a stream or vector of values. + *

+ * This behaves the same as an {@link Observable} except that it can only emit either a single successful value, or an error. + *

+ * Like an {@link Observable} it is lazy, can be either "hot" or "cold", synchronous or asynchronous. + *

+ * The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: + *

+ * + *

+ * For more information see the ReactiveX documentation. + * + * @param + * the type of the item emitted by the Single + */ +@Experimental +public class Single { + + final Observable.OnSubscribe onSubscribe; + + /** + * Creates a Single with a Function to execute when it is subscribed to (executed). + *

+ * Note: Use {@link #create(OnExecute)} to create a Single, instead of this constructor, + * unless you specifically have a need for inheritance. + * + * @param f + * {@link OnExecute} to be executed when {@link #execute(SingleSubscriber)} or {@link #subscribe(Subscriber)} is called + */ + protected Single(final OnSubscribe f) { + // bridge between OnSubscribe (which all Operators and Observables use) and OnExecute (for Single) + this.onSubscribe = new Observable.OnSubscribe() { + + @Override + public void call(final Subscriber child) { + final SingleDelayedProducer producer = new SingleDelayedProducer(child); + child.setProducer(producer); + SingleSubscriber ss = new SingleSubscriber() { + + @Override + public void onSuccess(T value) { + producer.setValue(value); + } + + @Override + public void onError(Throwable error) { + child.onError(error); + } + + }; + child.add(ss); + f.call(ss); + } + + }; + } + + private Single(final Observable.OnSubscribe f) { + this.onSubscribe = f; + } + + private static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); + + /** + * Returns a Single that will execute the specified function when a {@link SingleSubscriber} executes it or {@link Subscriber} subscribes to it. + *

+ * + *

+ * Write the function you pass to {@code create} so that it behaves as a Single: It should invoke the + * SingleSubscriber {@link SingleSubscriber#onSuccess onSuccess} and {@link SingleSubscriber#onError onError} methods appropriately. + *

+ * A well-formed Single must invoke either the SingleSubscriber's {@code onSuccess} method exactly once or + * its {@code onError} method exactly once. + *

+ *

+ *
Scheduler:
+ *
{@code create} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of the item that this Single emits + * @param f + * a function that accepts an {@code SingleSubscriber}, and invokes its {@code onSuccess} or {@code onError} methods as appropriate + * @return a Single that, when a {@link Subscriber} subscribes to it, will execute the specified function + * @see ReactiveX operators documentation: Create + */ + public final static Single create(OnSubscribe f) { + return new Single(f); // TODO need hook + } + + /** + * Invoked when Single.execute is called. + */ + public static interface OnSubscribe extends Action1> { + // cover for generics insanity + } + + /** + * Lifts a function to the current Single and returns a new Single that when subscribed to will pass + * the values of the current Single through the Operator function. + *

+ * In other words, this allows chaining TaskExecutors together on a Single for acting on the values within + * the Single. + *

{@code + * task.map(...).filter(...).lift(new OperatorA()).lift(new OperatorB(...)).subscribe() + * }

+ * If the operator you are creating is designed to act on the item emitted by a source + * Single, use {@code lift}. If your operator is designed to transform the source Single as a whole + * (for instance, by applying a particular set of existing RxJava operators to it) use {@link #compose}. + *

+ *
Scheduler:
+ *
{@code lift} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param lift + * the Operator that implements the Single-operating function to be applied to the source Single + * @return a Single that is the result of applying the lifted Operator to the source Single + * @see RxJava wiki: Implementing Your Own Operators + */ + private final 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 + + return new Single(new Observable.OnSubscribe() { + @Override + public void call(Subscriber o) { + try { + final Subscriber st = hook.onLift(lift).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 + if (e instanceof OnErrorNotImplementedException) { + throw (OnErrorNotImplementedException) e; + } + st.onError(e); + } + } catch (Throwable e) { + if (e instanceof OnErrorNotImplementedException) { + throw (OnErrorNotImplementedException) 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); + } + } + }); + } + + /** + * Transform an Observable by applying a particular Transformer function to it. + *

+ * This method operates on the Observable itself whereas {@link #lift} operates on the Observable's + * Subscribers or Observers. + *

+ * If the operator you are creating is designed to act on the individual items emitted by a source + * Observable, use {@link #lift}. If your operator is designed to transform the source Observable as a whole + * (for instance, by applying a particular set of existing RxJava operators to it) use {@code compose}. + *

+ *
Scheduler:
+ *
{@code compose} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param transformer + * implements the function that transforms the source Observable + * @return the source Observable, transformed by the transformer function + * @see RxJava wiki: Implementing Your Own Operators + */ + @SuppressWarnings("unchecked") + public Single compose(Transformer transformer) { + return ((Transformer) transformer).call(this); + } + + /** + * Transformer function used by {@link #compose}. + * + * @warn more complete description needed + */ + public static interface Transformer extends Func1, Single> { + // cover for generics insanity + } + + private static Observable toObservable(Single t) { + // is this sufficient, or do I need to keep the outer Single and subscribe to it? + return Observable.create(t.onSubscribe); + } + + /** + * INTERNAL: Used with lift and operators. + * + * Converts the source {@code Single} into an {@code Single>} that emits the + * source Observable as its single emission. + *

+ * + *

+ *
Scheduler:
+ *
{@code nest} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @return an Observable that emits a single item: the source Observable + * @see ReactiveX operators documentation: To + */ + private final Single> nest() { + return Single.just(toObservable(this)); + } + + /* ********************************************************************************************************* + * Operators Below Here + * ********************************************************************************************************* + */ + + /** + * Returns an Observable that emits the items emitted by two Tasks, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * an Single to be concatenated + * @param t2 + * an Single to be concatenated + * @return an Observable that emits items emitted by the two source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2) { + return Observable.concat(toObservable(t1), toObservable(t2)); + } + + /** + * Returns an Observable that emits the items emitted by three Tasks, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated + * @param t2 + * a Single to be concatenated + * @param t3 + * a Single to be concatenated + * @return an Observable that emits items emitted by the three source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2, Single t3) { + return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3)); + } + + /** + * Returns an Observable that emits the items emitted by four Observables, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated + * @param t2 + * a Single to be concatenated + * @param t3 + * a Single to be concatenated + * @param t4 + * a Single to be concatenated + * @return an Observable that emits items emitted by the four source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2, Single t3, Single t4) { + return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4)); + } + + /** + * Returns an Observable that emits the items emitted by five Observables, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated + * @param t2 + * a Single to be concatenated + * @param t3 + * a Single to be concatenated + * @param t4 + * a Single to be concatenated + * @param t5 + * a Single to be concatenated + * @return an Observable that emits items emitted by the five source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5) { + return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5)); + } + + /** + * Returns an Observable that emits the items emitted by six Observables, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated + * @param t2 + * a Single to be concatenated + * @param t3 + * a Single to be concatenated + * @param t4 + * a Single to be concatenated + * @param t5 + * a Single to be concatenated + * @param t6 + * a Single to be concatenated + * @return an Observable that emits items emitted by the six source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { + return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6)); + } + + /** + * Returns an Observable that emits the items emitted by seven Observables, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated + * @param t2 + * a Single to be concatenated + * @param t3 + * a Single to be concatenated + * @param t4 + * a Single to be concatenated + * @param t5 + * a Single to be concatenated + * @param t6 + * a Single to be concatenated + * @param t7 + * a Single to be concatenated + * @return an Observable that emits items emitted by the seven source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { + return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7)); + } + + /** + * Returns an Observable that emits the items emitted by eight Observables, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated + * @param t2 + * a Single to be concatenated + * @param t3 + * a Single to be concatenated + * @param t4 + * a Single to be concatenated + * @param t5 + * a Single to be concatenated + * @param t6 + * a Single to be concatenated + * @param t7 + * a Single to be concatenated + * @param t8 + * a Single to be concatenated + * @return an Observable that emits items emitted by the eight source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { + return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8)); + } + + /** + * Returns an Observable that emits the items emitted by nine Observables, one after the other, without + * interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated + * @param t2 + * a Single to be concatenated + * @param t3 + * a Single to be concatenated + * @param t4 + * a Single to be concatenated + * @param t5 + * a Single to be concatenated + * @param t6 + * a Single to be concatenated + * @param t7 + * a Single to be concatenated + * @param t8 + * a Single to be concatenated + * @param t9 + * a Single to be concatenated + * @return an Observable that emits items emitted by the nine source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { + return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8), toObservable(t9)); + } + + /** + * Returns an Observable that invokes an {@link Observer}'s {@link Observer#onError onError} method when the + * Observer subscribes to it. + *

+ * + *

+ *
Scheduler:
+ *
{@code error} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param exception + * the particular Throwable to pass to {@link Observer#onError onError} + * @param + * the type of the items (ostensibly) emitted by the Observable + * @return an Observable that invokes the {@link Observer}'s {@link Observer#onError onError} method when + * the Observer subscribes to it + * @see ReactiveX operators documentation: Throw + */ + public final static Single error(final Throwable exception) { + return Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber te) { + te.onError(exception); + } + + }); + } + + /** + * Converts a {@link Future} into an Observable. + *

+ * + *

+ * You can convert any object that supports the {@link Future} interface into an Observable that emits the + * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. + *

+ * Important note: This Observable is blocking; you cannot unsubscribe from it. + *

+ *
Scheduler:
+ *
{@code from} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param future + * the source {@link Future} + * @param + * the type of object that the {@link Future} returns, and also the type of item to be emitted by + * the resulting Observable + * @return an Observable that emits the item from the source {@link Future} + * @see ReactiveX operators documentation: From + */ + public final static Single from(Future future) { + return new Single(OnSubscribeToObservableFuture.toObservableFuture(future)); + } + + /** + * Converts a {@link Future} into an Observable, with a timeout on the Future. + *

+ * + *

+ * You can convert any object that supports the {@link Future} interface into an Observable that emits the + * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. + *

+ * Important note: This Observable is blocking; you cannot unsubscribe from it. + *

+ *
Scheduler:
+ *
{@code from} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param future + * the source {@link Future} + * @param timeout + * the maximum time to wait before calling {@code get} + * @param unit + * the {@link TimeUnit} of the {@code timeout} argument + * @param + * the type of object that the {@link Future} returns, and also the type of item to be emitted by + * the resulting Observable + * @return an Observable that emits the item from the source {@link Future} + * @see ReactiveX operators documentation: From + */ + public final static Single from(Future future, long timeout, TimeUnit unit) { + return new Single(OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); + } + + /** + * Converts a {@link Future}, operating on a specified {@link Scheduler}, into an Observable. + *

+ * + *

+ * You can convert any object that supports the {@link Future} interface into an Observable that emits the + * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param future + * the source {@link Future} + * @param scheduler + * the {@link Scheduler} to wait for the Future on. Use a Scheduler such as {@link Schedulers#io()} that can block and wait on the Future + * @param + * the type of object that the {@link Future} returns, and also the type of item to be emitted by + * the resulting Observable + * @return an Observable that emits the item from the source {@link Future} + * @see ReactiveX operators documentation: From + */ + public final static Single from(Future future, Scheduler scheduler) { + return new Single(OnSubscribeToObservableFuture.toObservableFuture(future)).subscribeOn(scheduler); + } + + /** + * Returns an Observable that emits a single item and then completes. + *

+ * + *

+ * To convert any object into an Observable that emits that object, pass that object into the {@code just} method. + *

+ * This is similar to the {@link #from(java.lang.Object[])} method, except that {@code from} will convert + * an {@link Iterable} object into an Observable that emits each of the items in the Iterable, one at a + * time, while the {@code just} method converts an Iterable into an Observable that emits the entire + * Iterable as a single item. + *

+ *
Scheduler:
+ *
{@code just} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param value + * the item to emit + * @param + * the type of that item + * @return an Observable that emits {@code value} as a single item and then completes + * @see ReactiveX operators documentation: Just + */ + public final static Single just(final T value) { + // TODO add similar optimization as ScalarSynchronousObservable + return Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber te) { + te.onSuccess(value); + } + + }); + } + + /** + * Flattens a Single that emits a Single into a single Single that emits the items emitted by + * the nested Single, without any transformation. + *

+ * + *

+ *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param source + * a Single that emits a Single + * @return a Single that emits the item that is the result of flattening the Single emitted by the {@code source} Single + * @see ReactiveX operators documentation: Merge + */ + public final static Single merge(final Single> source) { + return Single.create(new OnSubscribe() { + + @Override + public void call(final SingleSubscriber child) { + source.subscribe(new SingleSubscriber>() { + + @Override + public void onSuccess(Single innerSingle) { + innerSingle.subscribe(child); + } + + @Override + public void onError(Throwable error) { + child.onError(error); + } + + }); + } + }); + } + + /** + * Flattens two Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2) { + return Observable.merge(toObservable(t1), toObservable(t2)); + } + + /** + * Flattens three Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @param t3 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2, Single t3) { + return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3)); + } + + /** + * Flattens four Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @param t3 + * a Single to be merged + * @param t4 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2, Single t3, Single t4) { + return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4)); + } + + /** + * Flattens five Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @param t3 + * a Single to be merged + * @param t4 + * a Single to be merged + * @param t5 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5) { + return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5)); + } + + /** + * Flattens six Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @param t3 + * a Single to be merged + * @param t4 + * a Single to be merged + * @param t5 + * a Single to be merged + * @param t6 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { + return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6)); + } + + /** + * Flattens seven Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @param t3 + * a Single to be merged + * @param t4 + * a Single to be merged + * @param t5 + * a Single to be merged + * @param t6 + * a Single to be merged + * @param t7 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { + return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7)); + } + + /** + * Flattens eight Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @param t3 + * a Single to be merged + * @param t4 + * a Single to be merged + * @param t5 + * a Single to be merged + * @param t6 + * a Single to be merged + * @param t7 + * a Single to be merged + * @param t8 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { + return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8)); + } + + /** + * Flattens nine Observables into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @param t2 + * a Single to be merged + * @param t3 + * a Single to be merged + * @param t4 + * a Single to be merged + * @param t5 + * a Single to be merged + * @param t6 + * a Single to be merged + * @param t7 + * a Single to be merged + * @param t8 + * a Single to be merged + * @param t9 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { + return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8), toObservable(t9)); + } + + /** + * 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. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by {@code o1} and the first item + * emitted by {@code o2}; the second item emitted by the new Observable will be the result of the function + * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. + *

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

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results + * in an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, final Func2 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the results of a specified combiner function applied to combinations of + * three items emitted, in sequence, by three other Observables. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, and the first item emitted by {@code o3}; the second item emitted by the new + * Observable will be the result of the function applied to the second item emitted by {@code o1}, the + * second item emitted by {@code o2}, and the second item emitted by {@code o3}; and so forth. + *

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

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param o3 + * a third source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results in + * an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, Single o3, Func3 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the results of a specified combiner function applied to combinations of + * four items emitted, in sequence, by four other Observables. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, the first item emitted by {@code o3}, and the first item emitted by {@code 04}; + * the second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that + * emits the fewest + * items. + *

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param o3 + * a third source Observable + * @param o4 + * a fourth source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results in + * an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, Single o3, Single o4, Func4 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the results of a specified combiner function applied to combinations of + * five items emitted, in sequence, by five other Observables. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, the first item emitted by {@code o3}, the first item emitted by {@code o4}, and + * the first item emitted by {@code o5}; the second item emitted by the new Observable will be the result of + * the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that + * emits the fewest + * items. + *

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param o3 + * a third source Observable + * @param o4 + * a fourth source Observable + * @param o5 + * a fifth source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results in + * an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Func5 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the results of a specified combiner function applied to combinations of + * six items emitted, in sequence, by six other Observables. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by each source Observable, the + * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that + * emits the fewest + * items. + *

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param o3 + * a third source Observable + * @param o4 + * a fourth source Observable + * @param o5 + * a fifth source Observable + * @param o6 + * a sixth source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results in + * an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, + Func6 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the results of a specified combiner function applied to combinations of + * seven items emitted, in sequence, by seven other Observables. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by each source Observable, the + * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that + * emits the fewest + * items. + *

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param o3 + * a third source Observable + * @param o4 + * a fourth source Observable + * @param o5 + * a fifth source Observable + * @param o6 + * a sixth source Observable + * @param o7 + * a seventh source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results in + * an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, + Func7 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6), toObservable(o7) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the results of a specified combiner function applied to combinations of + * eight items emitted, in sequence, by eight other Observables. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by each source Observable, the + * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that + * emits the fewest + * items. + *

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param o3 + * a third source Observable + * @param o4 + * a fourth source Observable + * @param o5 + * a fifth source Observable + * @param o6 + * a sixth source Observable + * @param o7 + * a seventh source Observable + * @param o8 + * an eighth source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results in + * an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, + Func8 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6), toObservable(o7), toObservable(o8) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the results of a specified combiner function applied to combinations of + * nine items emitted, in sequence, by nine other Observables. + *

+ * + *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable + * will be the result of the function applied to the first item emitted by each source Observable, the + * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that + * emits the fewest + * items. + *

+ *
Scheduler:
+ *
{@code zip} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param o1 + * the first source Observable + * @param o2 + * a second source Observable + * @param o3 + * a third source Observable + * @param o4 + * a fourth source Observable + * @param o5 + * a fifth source Observable + * @param o6 + * a sixth source Observable + * @param o7 + * a seventh source Observable + * @param o8 + * an eighth source Observable + * @param o9 + * a ninth source Observable + * @param zipFunction + * a function that, when applied to an item emitted by each of the source Observables, results in + * an item that will be emitted by the resulting Observable + * @return an Observable that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, + Single o9, Func9 zipFunction) { + return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6), toObservable(o7), toObservable(o8), toObservable(o9) }).lift(new OperatorZip(zipFunction)); + } + + /** + * Returns an Observable that emits the items emitted from the current Observable, then the next, one after + * the other, without interleaving them. + *

+ * + *

+ *
Scheduler:
+ *
{@code concat} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be concatenated after the current + * @return an Observable that emits items emitted by the two source Observables, one after the other, + * without interleaving them + * @see ReactiveX operators documentation: Concat + */ + public final Observable concatWith(Single t1) { + return concat(this, t1); + } + + /** + * Returns an Observable that emits items based on applying a function that you supply to each item emitted + * by the source Observable, where that function returns an Observable, and then merging those resulting + * Observables and emitting the results of this merger. + *

+ * + *

+ *
Scheduler:
+ *
{@code flatMap} 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 result of applying the transformation function to each item emitted + * by the source Observable and merging the results of the Observables obtained from this + * transformation + * @see ReactiveX operators documentation: FlatMap + */ + public final Single flatMap(final Func1> func) { + return merge(map(func)); + } + + /** + * Returns an Observable that emits items based on applying a function that you supply to each item emitted + * by the source Observable, where that function returns an Observable, and then merging those resulting + * Observables and emitting the results of this merger. + *

+ * + *

+ *
Scheduler:
+ *
{@code flatMap} 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 result of applying the transformation function to each item emitted + * by the source Observable and merging the results of the Observables obtained from this + * transformation + * @see ReactiveX operators documentation: FlatMap + */ + public final Observable flatMapObservable(Func1> func) { + return Observable.merge(toObservable(map(func))); + } + + /** + * Returns a Single that applies a specified function to the item emitted by the source Single and + * emits the result of this function applications. + *

+ * + *

+ *
Scheduler:
+ *
{@code map} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param func + * a function to apply to the item emitted by the Single + * @return a Single that emits the item from the source Single, transformed by the specified function + * @see ReactiveX operators documentation: Map + */ + public final Single map(Func1 func) { + return lift(new OperatorMap(func)); + } + + /** + * Flattens this and another Observable into a single Observable, without any transformation. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code mergeWith} method. + *

+ *
Scheduler:
+ *
{@code mergeWith} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param t1 + * a Single to be merged + * @return an Observable that emits all of the items emitted by the source Observables + * @see ReactiveX operators documentation: Merge + */ + public final Observable mergeWith(Single t1) { + return merge(this, t1); + } + + /** + * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, + * asynchronously with an unbounded buffer. + *

+ * + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param scheduler + * the {@link Scheduler} to notify {@link Observer}s on + * @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 + */ + public final Single observeOn(Scheduler scheduler) { + return lift(new OperatorObserveOn(scheduler)); + } + + /** + * Instructs an Observable to emit an item (returned by a specified function) rather than invoking {@link Observer#onError onError} if it encounters an error. + *

+ * + *

+ * By default, when an Observable encounters an error that prevents it from emitting the expected item to + * its {@link Observer}, the Observable invokes its Observer's {@code onError} method, and then quits + * without invoking any more of its Observer's methods. The {@code onErrorReturn} method changes this + * behavior. If you pass a function ({@code resumeFunction}) to an Observable's {@code onErrorReturn} method, if the original Observable encounters an error, instead of invoking its Observer's + * {@code onError} method, it will instead emit the return value of {@code resumeFunction}. + *

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

+ *
Scheduler:
+ *
{@code onErrorReturn} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param resumeFunction + * a function that returns an item that the new Observable will emit if the source Observable + * encounters an error + * @return the original Observable with appropriately modified behavior + * @see ReactiveX operators documentation: Catch + */ + public final Single onErrorReturn(Func1 resumeFunction) { + return lift(new OperatorOnErrorReturn(resumeFunction)); + } + + /** + * Subscribes to an Observable but ignore its emissions and notifications. + *
+ *
Scheduler:
+ *
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @return a {@link Subscription} reference can request the {@link Single} stop work. + * @throws OnErrorNotImplementedException + * if the Observable tries to call {@code onError} + * @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 + } + + }); + } + + /** + * Subscribes to an Observable and provides a callback to handle the items it emits. + *
+ *
Scheduler:
+ *
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param onNext + * the {@code Action1} you have designed to accept emissions from the Observable + * @return a {@link Subscription} reference can request the {@link Single} stop work. + * @throws IllegalArgumentException + * if {@code onNext} is null + * @throws OnErrorNotImplementedException + * if the Observable tries to call {@code onError} + * @see ReactiveX operators documentation: Subscribe + */ + public final Subscription subscribe(final Action1 onSuccess) { + if (onSuccess == null) { + throw new IllegalArgumentException("onSuccess 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) { + onSuccess.call(args); + } + + }); + } + + /** + * Subscribes to an Observable and provides callbacks to handle the items it emits and any error + * notification it issues. + *
+ *
Scheduler:
+ *
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param onNext + * the {@code Action1} you have designed to accept emissions from the Observable + * @param onError + * the {@code Action1} you have designed to accept any error notification from the + * Observable + * @return a {@link Subscription} reference can request the {@link Single} stop work. + * @see ReactiveX operators documentation: Subscribe + * @throws IllegalArgumentException + * if {@code onNext} is null, or + * if {@code onError} is null + */ + public final Subscription subscribe(final Action1 onSuccess, final Action1 onError) { + if (onSuccess == null) { + throw new IllegalArgumentException("onSuccess can not be null"); + } + if (onError == null) { + 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) { + onSuccess.call(args); + } + + }); + } + + /** + * Subscribes to an Observable and invokes {@link OnSubscribe} function without any contract protection, + * error handling, unsubscribe, or execution hooks. + *

+ * Use this only for implementing an {@link Operator} that requires nested subscriptions. For other + * purposes, use {@link #subscribe(Subscriber)} which ensures the Rx contract and other functionality. + *

+ *
Scheduler:
+ *
{@code unsafeSubscribe} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param subscriber + * the Subscriber that will handle emissions and notifications from the Observable + */ + public final void 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); + } catch (Throwable e) { + // special handling for certain Throwable/Error/Exception types + Exceptions.throwIfFatal(e); + // if an unhandled error occurs executing the onSubscribe we will propagate it + try { + subscriber.onError(hook.onSubscribeError(e)); + } catch (OnErrorNotImplementedException e2) { + // special handling when onError is not implemented ... we just rethrow + throw e2; + } catch (Throwable e2) { + // if this happens it means the onError itself failed (perhaps an invalid function implementation) + // so we are unable to propagate the error correctly and will just throw + RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2); + // TODO could the hook be the cause of the error in the on error handling. + hook.onSubscribeError(r); + // TODO why aren't we throwing the hook's return value. + throw r; + } + } + } + + /** + * Subscribes to an Single and provides a Subscriber that implements functions to handle the item the + * Single emits and any error notification it issues. + *

+ * A typical implementation of {@code subscribe} does the following: + *

    + *
  1. It stores a reference to the Subscriber in a collection object, such as a {@code List} object.
  2. + *
  3. It returns a reference to the {@link Subscription} interface. This enables Subscribers to + * unsubscribe, that is, to stop receiving items and notifications before the Observable completes, which + * also invokes the Subscriber's {@link Subscriber#onCompleted onCompleted} method.
  4. + *

+ * An {@code Single} instance is responsible for accepting all subscriptions and notifying all + * Subscribers. Unless the documentation for a particular {@code Single} implementation indicates + * otherwise, Subscriber should make no assumptions about the order in which multiple Subscribers will + * receive their notifications. + *

+ * For more information see the + * ReactiveX documentation. + *

+ *
Scheduler:
+ *
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param subscriber + * the {@link Subscriber} that will handle emissions and notifications from the Observable + * @return a {@link Subscription} reference can request the {@link Single} stop work. + * @throws IllegalStateException + * if {@code subscribe} is unable to obtain an {@code OnSubscribe<>} function + * @throws IllegalArgumentException + * if the {@link Subscriber} provided as the argument to {@code subscribe} is {@code null} + * @throws OnErrorNotImplementedException + * if the {@link Subscriber}'s {@code onError} method is null + * @throws RuntimeException + * if the {@link Subscriber}'s {@code onError} method itself threw a {@code Throwable} + * @see ReactiveX operators documentation: Subscribe + */ + public final Subscription subscribe(Subscriber subscriber) { + // validate and proceed + if (subscriber == null) { + throw new IllegalArgumentException("observer can not be null"); + } + if (onSubscribe == null) { + throw new IllegalStateException("onSubscribe function can not be null."); + /* + * the subscribe function can also be overridden but generally that's not the appropriate approach + * so I won't mention that in the exception + */ + } + + // new Subscriber so onStart it + subscriber.onStart(); + + /* + * See https://github.com/ReactiveX/RxJava/issues/216 for discussion on "Guideline 6.4: Protect calls + * to user code from within an Observer" + */ + // if not already wrapped + if (!(subscriber instanceof SafeSubscriber)) { + // assign to `observer` so we return the protected version + 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. + try { + // allow the hook to intercept and/or decorate + // TODO add back the hook + // hook.onSubscribeStart(this, onSubscribe).call(subscriber); + onSubscribe.call(subscriber); + return hook.onSubscribeReturn(subscriber); + } catch (Throwable e) { + // special handling for certain Throwable/Error/Exception types + Exceptions.throwIfFatal(e); + // if an unhandled error occurs executing the onSubscribe we will propagate it + try { + subscriber.onError(hook.onSubscribeError(e)); + } catch (OnErrorNotImplementedException e2) { + // special handling when onError is not implemented ... we just rethrow + throw e2; + } catch (Throwable e2) { + // if this happens it means the onError itself failed (perhaps an invalid function implementation) + // so we are unable to propagate the error correctly and will just throw + RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2); + // TODO could the hook be the cause of the error in the on error handling. + hook.onSubscribeError(r); + // TODO why aren't we throwing the hook's return value. + throw r; + } + return Subscriptions.empty(); + } + } + + /** + * Subscribes to an Single and provides a SingleSubscriber that implements functions to handle the item the + * Single emits and any error notification it issues. + *

+ * A typical implementation of {@code subscribe} does the following: + *

    + *
  1. It stores a reference to the Subscriber in a collection object, such as a {@code List} object.
  2. + *
  3. It returns a reference to the {@link Subscription} interface. This enables Subscribers to + * unsubscribe, that is, to stop receiving items and notifications before the Observable completes, which + * also invokes the Subscriber's {@link Subscriber#onCompleted onCompleted} method.
  4. + *

+ * An {@code Single} instance is responsible for accepting all subscriptions and notifying all + * Subscribers. Unless the documentation for a particular {@code Single} implementation indicates + * otherwise, Subscriber should make no assumptions about the order in which multiple Subscribers will + * receive their notifications. + *

+ * For more information see the + * ReactiveX documentation. + *

+ *
Scheduler:
+ *
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param subscriber + * the {@link Subscriber} that will handle emissions and notifications from the Observable + * @return a {@link Subscription} reference can request the {@link Single} stop work. + * @throws IllegalStateException + * if {@code subscribe} is unable to obtain an {@code OnSubscribe<>} function + * @throws IllegalArgumentException + * if the {@link Subscriber} provided as the argument to {@code subscribe} is {@code null} + * @throws OnErrorNotImplementedException + * if the {@link Subscriber}'s {@code onError} method is null + * @throws RuntimeException + * if the {@link Subscriber}'s {@code onError} method itself threw a {@code Throwable} + * @see ReactiveX operators documentation: Subscribe + */ + public final Subscription subscribe(final SingleSubscriber te) { + Subscriber s = new Subscriber() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + te.onError(e); + } + + @Override + public void onNext(T t) { + te.onSuccess(t); + } + + }; + te.add(s); + subscribe(s); + return s; + } + + /** + * Asynchronously subscribes Observers to this Observable on the specified {@link Scheduler}. + *

+ * + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param scheduler + * the {@link Scheduler} to perform subscription actions on + * @return the source Observable modified so that its subscriptions happen on the + * specified {@link Scheduler} + * @see ReactiveX operators documentation: SubscribeOn + * @see RxJava Threading Examples + * @see #observeOn + */ + public final Single subscribeOn(Scheduler scheduler) { + return nest().lift(new OperatorSubscribeOn(scheduler)); + } + + /** + * Returns an Observable that mirrors the source Observable but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the resulting Observable terminates and notifies observers of a {@code TimeoutException}. + *

+ * + *

+ *
Scheduler:
+ *
This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.
+ *
+ * + * @param timeout + * maximum duration between emitted items before a timeout occurs + * @param timeUnit + * the unit of time that applies to the {@code timeout} argument. + * @return the source Observable modified to notify observers of a {@code TimeoutException} in case of a + * timeout + * @see ReactiveX operators documentation: Timeout + */ + public final Single timeout(long timeout, TimeUnit timeUnit) { + return timeout(timeout, timeUnit, null, Schedulers.computation()); + } + + /** + * Returns an Observable that mirrors the source Observable but applies a timeout policy for each emitted + * item, where this policy is governed on a specified Scheduler. If the next item isn't emitted within the + * specified timeout duration starting from its predecessor, the resulting Observable terminates and + * notifies observers of a {@code TimeoutException}. + *

+ * + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param timeout + * maximum duration between items before a timeout occurs + * @param timeUnit + * the unit of time that applies to the {@code timeout} argument + * @param scheduler + * the Scheduler to run the timeout timers on + * @return the source Observable modified to notify observers of a {@code TimeoutException} in case of a + * timeout + * @see ReactiveX operators documentation: Timeout + */ + public final Single timeout(long timeout, TimeUnit timeUnit, Scheduler scheduler) { + return timeout(timeout, timeUnit, null, scheduler); + } + + /** + * Returns an Observable that mirrors the source Observable but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the resulting Observable begins instead to mirror a fallback Observable. + *

+ * + *

+ *
Scheduler:
+ *
This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.
+ *
+ * + * @param timeout + * maximum duration between items before a timeout occurs + * @param timeUnit + * the unit of time that applies to the {@code timeout} argument + * @param other + * the fallback Observable to use in case of a timeout + * @return the source Observable modified to switch to the fallback Observable in case of a timeout + * @see ReactiveX operators documentation: Timeout + */ + public final Single timeout(long timeout, TimeUnit timeUnit, Single other) { + return timeout(timeout, timeUnit, other, Schedulers.computation()); + } + + /** + * Returns an Observable that mirrors the source Observable but applies a timeout policy for each emitted + * item using a specified Scheduler. If the next item isn't emitted within the specified timeout duration + * starting from its predecessor, the resulting Observable begins instead to mirror a fallback Observable. + *

+ * + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param timeout + * maximum duration between items before a timeout occurs + * @param timeUnit + * the unit of time that applies to the {@code timeout} argument + * @param other + * the Observable to use as the fallback in case of a timeout + * @param scheduler + * the {@link Scheduler} to run the timeout timers on + * @return the source Observable modified so that it will switch to the fallback Observable in case of a + * timeout + * @see ReactiveX operators documentation: Timeout + */ + public final Single timeout(long timeout, TimeUnit timeUnit, Single other, Scheduler scheduler) { + if (other == null) { + other = Single. error(new TimeoutException()); + } + return lift(new OperatorTimeout(timeout, timeUnit, toObservable(other), scheduler)); + } + + /** + * Returns an Observable that emits items that are the result of applying a specified function to pairs of + * values, one each from the source Observable and another specified Observable. + *

+ * + *

+ *
Scheduler:
+ *
{@code zipWith} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param + * the type of items emitted by the {@code other} Observable + * @param + * the type of items emitted by the resulting Observable + * @param other + * the other Observable + * @param zipFunction + * a function that combines the pairs of items from the two Observables to generate the items to + * be emitted by the resulting Observable + * @return an Observable that pairs up values from the source Observable and the {@code other} Observable + * and emits the results of {@code zipFunction} applied to these pairs + * @see ReactiveX operators documentation: Zip + */ + public final Single zipWith(Single other, Func2 zipFunction) { + return zip(this, other, zipFunction); + } + +} diff --git a/src/main/java/rx/SingleSubscriber.java b/src/main/java/rx/SingleSubscriber.java new file mode 100644 index 0000000000..01fa84a8c1 --- /dev/null +++ b/src/main/java/rx/SingleSubscriber.java @@ -0,0 +1,81 @@ +/** + * Copyright 2015 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.internal.util.SubscriptionList; + +/** + * Provides a mechanism for receiving push-based notifications. + *

+ * After an SingleSubscriber calls an {@link Single}'s {@link Single#subscribe subscribe} method, the + * {@code Single} calls the SingleSubscriber's {@link #onSuccess} and {@link #onError} methods to provide notifications. + * A well-behaved {@code Single} will call an SingleSubscriber's {@link #onSuccess} method exactly once or + * the SingleSubscriber's {@link #onError} method exactly once. + * + * @see ReactiveX documentation: Observable + * @param + * the type of item the SingleSubscriber expects to observe + */ +@Experimental +public abstract class SingleSubscriber implements Subscription { + + private final SubscriptionList cs = new SubscriptionList(); + + /** + * Notifies the SingleSubscriber with a single item and that the {@link Single} has finished sending push-based notifications. + *

+ * The {@link Single} will not call this method if it calls {@link #onError}. + */ + public abstract void onSuccess(T value); + + /** + * Notifies the SingleSubscriber that the {@link Single} has experienced an error condition. + *

+ * If the {@link Single} calls this method, it will not thereafter call {@link #onSuccess}. + * + * @param e + * the exception encountered by the Single + */ + public abstract void onError(Throwable error); + + /** + * Adds a {@link Subscription} to this Subscriber's list of subscriptions if this list is not marked as + * unsubscribed. If the list is marked as unsubscribed, {@code add} will indicate this by + * explicitly unsubscribing the new {@code Subscription} as well. + * + * @param s + * the {@code Subscription} to add + */ + public final void add(Subscription s) { + cs.add(s); + } + + @Override + public final void unsubscribe() { + cs.unsubscribe(); + } + + /** + * Indicates whether this Subscriber has unsubscribed from its list of subscriptions. + * + * @return {@code true} if this Subscriber has unsubscribed from its subscriptions, {@code false} otherwise + */ + @Override + public final boolean isUnsubscribed() { + return cs.isUnsubscribed(); + } +} \ No newline at end of file diff --git a/src/perf/java/rx/PerfBaseline.java b/src/perf/java/rx/ObservablePerfBaseline.java similarity index 98% rename from src/perf/java/rx/PerfBaseline.java rename to src/perf/java/rx/ObservablePerfBaseline.java index fa3f4bc313..061caf6264 100644 --- a/src/perf/java/rx/PerfBaseline.java +++ b/src/perf/java/rx/ObservablePerfBaseline.java @@ -30,7 +30,7 @@ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) -public class PerfBaseline { +public class ObservablePerfBaseline { @State(Scope.Thread) public static class Input extends InputWithIncrementingInteger { diff --git a/src/perf/java/rx/SinglePerfBaseline.java b/src/perf/java/rx/SinglePerfBaseline.java new file mode 100644 index 0000000000..e1a646cef0 --- /dev/null +++ b/src/perf/java/rx/SinglePerfBaseline.java @@ -0,0 +1,100 @@ +/** + * 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; + +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.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import rx.Single.OnSubscribe; +import rx.jmh.LatchedObserver; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class SinglePerfBaseline { + + + @Benchmark + public void singleConsumption(Input input) throws InterruptedException { + input.single.subscribe(input.newSubscriber()); + } + + @Benchmark + public void singleConsumptionUnsafe(Input input) throws InterruptedException { + input.single.unsafeSubscribe(input.newSubscriber()); + } + + @Benchmark + public void newSingleAndSubscriberEachTime(Input input) throws InterruptedException { + input.newSingle().subscribe(input.newSubscriber()); + } + + @State(Scope.Thread) + public static class Input { + public Single single; + public Blackhole bh; + + @Setup + public void setup(final Blackhole bh) { + this.bh = bh; + single = Single.just(1); + } + + public LatchedObserver newLatchedObserver() { + return new LatchedObserver(bh); + } + + public Single newSingle() { + return Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber t) { + t.onSuccess(1); + } + + }); + } + + public Subscriber newSubscriber() { + return new Subscriber() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + bh.consume(t); + } + + }; + } + + } +} diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java new file mode 100644 index 0000000000..778feffb3c --- /dev/null +++ b/src/test/java/rx/SingleTest.java @@ -0,0 +1,455 @@ +/** + * Copyright 2015 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import rx.Single.OnSubscribe; +import rx.functions.Action0; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.subscriptions.Subscriptions; + +public class SingleTest { + + @Test + public void testHelloWorld() { + TestSubscriber ts = new TestSubscriber(); + Single.just("Hello World!").subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList("Hello World!")); + } + + @Test + public void testHelloWorld2() { + final AtomicReference v = new AtomicReference(); + Single.just("Hello World!").subscribe(new SingleSubscriber() { + + @Override + public void onSuccess(String value) { + v.set(value); + } + + @Override + public void onError(Throwable error) { + + } + + }); + assertEquals("Hello World!", v.get()); + } + + @Test + public void testMap() { + TestSubscriber ts = new TestSubscriber(); + Single.just("A") + .map(new Func1() { + + @Override + public String call(String s) { + return s + "B"; + } + + }) + .subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList("AB")); + } + + @Test + public void testZip() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just("A"); + Single b = Single.just("B"); + + Single.zip(a, b, new Func2() { + + @Override + public String call(String a, String b) { + return a + b; + } + + }) + .subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList("AB")); + } + + @Test + public void testZipWith() { + TestSubscriber ts = new TestSubscriber(); + + Single.just("A").zipWith(Single.just("B"), new Func2() { + + @Override + public String call(String a, String b) { + return a + b; + } + + }) + .subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList("AB")); + } + + @Test + public void testMerge() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just("A"); + Single b = Single.just("B"); + + Single.merge(a, b).subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList("A", "B")); + } + + @Test + public void testMergeWith() { + TestSubscriber ts = new TestSubscriber(); + + Single.just("A").mergeWith(Single.just("B")).subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList("A", "B")); + } + + @Test + public void testCreateSuccess() { + TestSubscriber ts = new TestSubscriber(); + Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber s) { + s.onSuccess("Hello"); + } + + }).subscribe(ts); + ts.assertReceivedOnNext(Arrays.asList("Hello")); + } + + @Test + public void testCreateError() { + TestSubscriber ts = new TestSubscriber(); + Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber s) { + s.onError(new RuntimeException("fail")); + } + + }).subscribe(ts); + assertEquals(1, ts.getOnErrorEvents().size()); + } + + @Test + public void testAsync() { + TestSubscriber ts = new TestSubscriber(); + Single.just("Hello") + .subscribeOn(Schedulers.io()) + .map(new Func1() { + + @Override + public String call(String v) { + System.out.println("SubscribeOn Thread: " + Thread.currentThread()); + return v; + } + + }) + .observeOn(Schedulers.computation()) + .map(new Func1() { + + @Override + public String call(String v) { + System.out.println("ObserveOn Thread: " + Thread.currentThread()); + return v; + } + + }) + .subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList("Hello")); + } + + @Test + public void testFlatMap() { + TestSubscriber ts = new TestSubscriber(); + Single.just("Hello").flatMap(new Func1>() { + + @Override + public Single call(String s) { + return Single.just(s + " World!").subscribeOn(Schedulers.computation()); + } + + }).subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList("Hello World!")); + } + + @Test + public void testTimeout() { + TestSubscriber ts = new TestSubscriber(); + Single s = Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber s) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // ignore as we expect this for the test + } + s.onSuccess("success"); + } + + }).subscribeOn(Schedulers.io()); + + s.timeout(100, TimeUnit.MILLISECONDS).subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertError(TimeoutException.class); + } + + @Test + public void testTimeoutWithFallback() { + TestSubscriber ts = new TestSubscriber(); + Single s = Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber s) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // ignore as we expect this for the test + } + s.onSuccess("success"); + } + + }).subscribeOn(Schedulers.io()); + + s.timeout(100, TimeUnit.MILLISECONDS, Single.just("hello")).subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + ts.assertValue("hello"); + } + + @Test + public void testUnsubscribe() throws InterruptedException { + TestSubscriber ts = new TestSubscriber(); + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(2); + + Single s = Single.create(new OnSubscribe() { + + @Override + public void call(final SingleSubscriber s) { + final Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(5000); + s.onSuccess("success"); + } catch (InterruptedException e) { + interrupted.set(true); + latch.countDown(); + } + } + + }); + s.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + unsubscribed.set(true); + t.interrupt(); + latch.countDown(); + } + + })); + t.start(); + } + + }); + + s.subscribe(ts); + + Thread.sleep(100); + + ts.unsubscribe(); + + if (latch.await(1000, TimeUnit.MILLISECONDS)) { + assertTrue(unsubscribed.get()); + assertTrue(interrupted.get()); + } else { + fail("timed out waiting for latch"); + } + } + + /** + * Assert that unsubscribe propagates when passing in a SingleSubscriber and not a Subscriber + */ + @Test + public void testUnsubscribe2() throws InterruptedException { + SingleSubscriber ts = new SingleSubscriber() { + + @Override + public void onSuccess(String value) { + // not interested in value + } + + @Override + public void onError(Throwable error) { + // not interested in value + } + + }; + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(2); + + Single s = Single.create(new OnSubscribe() { + + @Override + public void call(final SingleSubscriber s) { + final Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(5000); + s.onSuccess("success"); + } catch (InterruptedException e) { + interrupted.set(true); + latch.countDown(); + } + } + + }); + s.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + unsubscribed.set(true); + t.interrupt(); + latch.countDown(); + } + + })); + t.start(); + } + + }); + + s.subscribe(ts); + + Thread.sleep(100); + + ts.unsubscribe(); + + if (latch.await(1000, TimeUnit.MILLISECONDS)) { + assertTrue(unsubscribed.get()); + assertTrue(interrupted.get()); + } else { + fail("timed out waiting for latch"); + } + } + + /** + * Assert that unsubscribe propagates when passing in a SingleSubscriber and not a Subscriber + */ + @Test + public void testUnsubscribeViaReturnedSubscription() throws InterruptedException { + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(2); + + Single s = Single.create(new OnSubscribe() { + + @Override + public void call(final SingleSubscriber s) { + final Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(5000); + s.onSuccess("success"); + } catch (InterruptedException e) { + interrupted.set(true); + latch.countDown(); + } + } + + }); + s.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + unsubscribed.set(true); + t.interrupt(); + latch.countDown(); + } + + })); + t.start(); + } + + }); + + Subscription subscription = s.subscribe(); + + Thread.sleep(100); + + subscription.unsubscribe(); + + if (latch.await(1000, TimeUnit.MILLISECONDS)) { + assertTrue(unsubscribed.get()); + assertTrue(interrupted.get()); + } else { + fail("timed out waiting for latch"); + } + } + + @Test + public void testBackpressureAsObservable() { + Single s = Single.create(new OnSubscribe() { + + @Override + public void call(SingleSubscriber t) { + t.onSuccess("hello"); + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(0); + } + }; + + s.subscribe(ts); + + ts.assertNoValues(); + + ts.requestMore(1); + + ts.assertValue("hello"); + } +} From 63b67d5de211d27a76071e50a7ad6c89cb97b90e Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 11 Jun 2015 11:16:16 +1000 Subject: [PATCH 300/857] fix awaitTerminalEventAndUnsubscribeOnTimeout --- .../java/rx/observers/TestSubscriber.java | 10 ++++-- .../java/rx/observers/TestSubscriberTest.java | 34 ++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 284002d452..e963b14cb4 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -281,7 +281,7 @@ public void awaitTerminalEvent(long timeout, TimeUnit unit) { * Blocks until this {@link Subscriber} receives a notification that the {@code Observable} is complete * (either an {@code onCompleted} or {@code onError} notification), or until a timeout expires; if the * Subscriber is interrupted before either of these events take place, this method unsubscribes the - * Subscriber from the Observable). + * Subscriber from the Observable). If timeout expires then the Subscriber is unsubscribed from the Observable. * * @param timeout * the duration of the timeout @@ -290,8 +290,12 @@ public void awaitTerminalEvent(long timeout, TimeUnit unit) { */ public void awaitTerminalEventAndUnsubscribeOnTimeout(long timeout, TimeUnit unit) { try { - awaitTerminalEvent(timeout, unit); - } catch (RuntimeException e) { + boolean result = latch.await(timeout, unit); + if (!result) { + // timeout occurred + unsubscribe(); + } + } catch (InterruptedException e) { unsubscribe(); } } diff --git a/src/test/java/rx/observers/TestSubscriberTest.java b/src/test/java/rx/observers/TestSubscriberTest.java index c07c261f77..75d59fc1f8 100644 --- a/src/test/java/rx/observers/TestSubscriberTest.java +++ b/src/test/java/rx/observers/TestSubscriberTest.java @@ -16,12 +16,16 @@ package rx.observers; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -29,6 +33,7 @@ import rx.Observable; import rx.Observer; +import rx.functions.Action0; import rx.subjects.PublishSubject; public class TestSubscriberTest { @@ -124,8 +129,35 @@ public void testWrappingMockWhenUnsubscribeInvolved() { @Test public void testAssertError() { RuntimeException e = new RuntimeException("Oops"); - TestSubscriber subscriber = new TestSubscriber(); + TestSubscriber subscriber = new TestSubscriber(); Observable.error(e).subscribe(subscriber); subscriber.assertError(e); } + + @Test + public void testAwaitTerminalEventWithDuration() { + TestSubscriber ts = new TestSubscriber(); + Observable.just(1).subscribe(ts); + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + ts.assertTerminalEvent(); + } + + @Test + public void testAwaitTerminalEventWithDurationAndUnsubscribeOnTimeout() { + TestSubscriber ts = new TestSubscriber(); + final AtomicBoolean unsub = new AtomicBoolean(false); + Observable.just(1) + // + .doOnUnsubscribe(new Action0() { + @Override + public void call() { + unsub.set(true); + } + }) + // + .delay(1000, TimeUnit.MILLISECONDS).subscribe(ts); + ts.awaitTerminalEventAndUnsubscribeOnTimeout(100, TimeUnit.MILLISECONDS); + assertTrue(unsub.get()); + } + } From 03336a980d738426fb76202750befa48f3c1599e Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 11 Jun 2015 14:05:50 -0700 Subject: [PATCH 301/857] Add marble diagrams for Single operators. --- src/main/java/rx/Single.java | 95 +++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index d8fcf88b87..7f788583de 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -53,7 +53,7 @@ *

* The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: *

- * + * *

* For more information see the ReactiveX documentation. * @@ -111,7 +111,7 @@ private Single(final Observable.OnSubscribe f) { /** * Returns a Single that will execute the specified function when a {@link SingleSubscriber} executes it or {@link Subscriber} subscribes to it. *

- * + * *

* Write the function you pass to {@code create} so that it behaves as a Single: It should invoke the * SingleSubscriber {@link SingleSubscriber#onSuccess onSuccess} and {@link SingleSubscriber#onError onError} methods appropriately. @@ -230,6 +230,9 @@ public static interface Transformer extends Func1, Single> { // cover for generics insanity } + /** + * + */ private static Observable toObservable(Single t) { // is this sufficient, or do I need to keep the outer Single and subscribe to it? return Observable.create(t.onSubscribe); @@ -241,7 +244,7 @@ private static Observable toObservable(Single t) { * Converts the source {@code Single} into an {@code Single>} that emits the * source Observable as its single emission. *

- * + * *

*
Scheduler:
*
{@code nest} does not operate by default on a particular {@link Scheduler}.
@@ -263,7 +266,7 @@ private final Single> nest() { * Returns an Observable that emits the items emitted by two Tasks, one after the other, without * interleaving them. *

- * + * *

*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -285,7 +288,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -309,7 +312,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -335,7 +338,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -363,7 +366,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -393,7 +396,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -425,7 +428,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -459,7 +462,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -495,7 +498,7 @@ public final static Observable concat(Single t1, Single - * + * *
*
Scheduler:
*
{@code error} does not operate by default on a particular {@link Scheduler}.
@@ -523,7 +526,7 @@ public void call(SingleSubscriber te) { /** * Converts a {@link Future} into an Observable. *

- * + * *

* You can convert any object that supports the {@link Future} interface into an Observable that emits the * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. @@ -549,7 +552,7 @@ public final static Single from(Future future) { /** * Converts a {@link Future} into an Observable, with a timeout on the Future. *

- * + * *

* You can convert any object that supports the {@link Future} interface into an Observable that emits the * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. @@ -579,7 +582,7 @@ public final static Single from(Future future, long timeout, /** * Converts a {@link Future}, operating on a specified {@link Scheduler}, into an Observable. *

- * + * *

* You can convert any object that supports the {@link Future} interface into an Observable that emits the * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. @@ -605,7 +608,7 @@ public final static Single from(Future future, Scheduler sch /** * Returns an Observable that emits a single item and then completes. *

- * + * *

* To convert any object into an Observable that emits that object, pass that object into the {@code just} method. *

@@ -641,7 +644,7 @@ public void call(SingleSubscriber te) { * Flattens a Single that emits a Single into a single Single that emits the items emitted by * the nested Single, without any transformation. *

- * + * *

*

*
Scheduler:
@@ -678,7 +681,7 @@ public void onError(Throwable error) { /** * Flattens two Observables into a single Observable, without any transformation. *

- * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -701,7 +704,7 @@ public final static Observable merge(Single t1, Single - * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -726,7 +729,7 @@ public final static Observable merge(Single t1, Single - * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -753,7 +756,7 @@ public final static Observable merge(Single t1, Single - * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -782,7 +785,7 @@ public final static Observable merge(Single t1, Single - * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -813,7 +816,7 @@ public final static Observable merge(Single t1, Single - * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -846,7 +849,7 @@ public final static Observable merge(Single t1, Single - * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -881,7 +884,7 @@ public final static Observable merge(Single t1, Single - * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code merge} method. @@ -919,7 +922,7 @@ public final static Observable merge(Single t1, Single - * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by {@code o1} and the first item * emitted by {@code o2}; the second item emitted by the new Observable will be the result of the function @@ -951,7 +954,7 @@ public final static Single zip(Single o1, Single - * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by {@code o1}, the first item * emitted by {@code o2}, and the first item emitted by {@code o3}; the second item emitted by the new @@ -986,7 +989,7 @@ public final static Single zip(Single o1, Singl * Returns an Observable that emits the results of a specified combiner function applied to combinations of * four items emitted, in sequence, by four other Observables. *

- * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by {@code o1}, the first item * emitted by {@code o2}, the first item emitted by {@code o3}, and the first item emitted by {@code 04}; @@ -1023,7 +1026,7 @@ public final static Single zip(Single o1, S * Returns an Observable that emits the results of a specified combiner function applied to combinations of * five items emitted, in sequence, by five other Observables. *

- * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by {@code o1}, the first item * emitted by {@code o2}, the first item emitted by {@code o3}, the first item emitted by {@code o4}, and @@ -1062,7 +1065,7 @@ public final static Single zip(Single o * Returns an Observable that emits the results of a specified combiner function applied to combinations of * six items emitted, in sequence, by six other Observables. *

- * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by each source Observable, the * second item emitted by the new Observable will be the result of the function applied to the second item @@ -1103,7 +1106,7 @@ public final static Single zip(Single - * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by each source Observable, the * second item emitted by the new Observable will be the result of the function applied to the second item @@ -1146,7 +1149,7 @@ public final static Single zip(Single - * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by each source Observable, the * second item emitted by the new Observable will be the result of the function applied to the second item @@ -1191,7 +1194,7 @@ public final static Single zip(Single - * + * *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable * will be the result of the function applied to the first item emitted by each source Observable, the * second item emitted by the new Observable will be the result of the function applied to the second item @@ -1238,7 +1241,7 @@ public final static Single zip(Single * Returns an Observable that emits the items emitted from the current Observable, then the next, one after * the other, without interleaving them. *

- * + * *

*
Scheduler:
*
{@code concat} does not operate by default on a particular {@link Scheduler}.
@@ -1259,7 +1262,7 @@ public final Observable concatWith(Single t1) { * by the source Observable, where that function returns an Observable, and then merging those resulting * Observables and emitting the results of this merger. *

- * + * *

*
Scheduler:
*
{@code flatMap} does not operate by default on a particular {@link Scheduler}.
@@ -1282,7 +1285,7 @@ public final Single flatMap(final Func1 - * + * *
*
Scheduler:
*
{@code flatMap} does not operate by default on a particular {@link Scheduler}.
@@ -1304,7 +1307,7 @@ public final Observable flatMapObservable(Func1 - * + * *
*
Scheduler:
*
{@code map} does not operate by default on a particular {@link Scheduler}.
@@ -1322,7 +1325,7 @@ public final Single map(Func1 func) { /** * Flattens this and another Observable into a single Observable, without any transformation. *

- * + * *

* You can combine items emitted by multiple Observables so that they appear as a single Observable, by * using the {@code mergeWith} method. @@ -1344,7 +1347,7 @@ public final Observable mergeWith(Single t1) { * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, * asynchronously with an unbounded buffer. *

- * + * *

*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
@@ -1364,7 +1367,7 @@ public final Single observeOn(Scheduler scheduler) { /** * Instructs an Observable to emit an item (returned by a specified function) rather than invoking {@link Observer#onError onError} if it encounters an error. *

- * + * *

* By default, when an Observable encounters an error that prevents it from emitting the expected item to * its {@link Observer}, the Observable invokes its Observer's {@code onError} method, and then quits @@ -1707,7 +1710,7 @@ public void onNext(T t) { /** * Asynchronously subscribes Observers to this Observable on the specified {@link Scheduler}. *

- * + * *

*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
@@ -1730,7 +1733,7 @@ public final Single subscribeOn(Scheduler scheduler) { * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, * the resulting Observable terminates and notifies observers of a {@code TimeoutException}. *

- * + * *

*
Scheduler:
*
This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.
@@ -1754,7 +1757,7 @@ public final Single timeout(long timeout, TimeUnit timeUnit) { * specified timeout duration starting from its predecessor, the resulting Observable terminates and * notifies observers of a {@code TimeoutException}. *

- * + * *

*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
@@ -1779,7 +1782,7 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Scheduler schedu * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, * the resulting Observable begins instead to mirror a fallback Observable. *

- * + * *

*
Scheduler:
*
This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.
@@ -1803,7 +1806,7 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single - * + * *
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
@@ -1832,7 +1835,7 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single - * + * *
*
Scheduler:
*
{@code zipWith} does not operate by default on a particular {@link Scheduler}.
From 7614c9c563cf6ad856e7a69c4624f1a578dd3bb5 Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 11 Jun 2015 15:15:27 -0700 Subject: [PATCH 302/857] Javadoc improvements. --- src/main/java/rx/Single.java | 666 +++++++++++++++-------------------- 1 file changed, 293 insertions(+), 373 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 7f788583de..8edb61febb 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -45,20 +45,25 @@ import rx.subscriptions.Subscriptions; /** - * The Single class that implements the Reactive Pattern for a single value response. See {@link Observable} for a stream or vector of values. + * The Single class implements the Reactive Pattern for a single value response. See {@link Observable} for the + * implementation of the Reactive Pattern for a stream or vector of values. *

- * This behaves the same as an {@link Observable} except that it can only emit either a single successful value, or an error. + * {@code Single} behaves the same as {@link Observable} except that it can only emit either a single successful + * value, or an error (there is no "onComplete" notification as there is for {@link Observable}) *

- * Like an {@link Observable} it is lazy, can be either "hot" or "cold", synchronous or asynchronous. + * Like an {@link Observable}, a {@code Single} is lazy, can be either "hot" or "cold", synchronous or + * asynchronous. *

* The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: *

* *

- * For more information see the ReactiveX documentation. + * For more information see the ReactiveX + * documentation. * * @param * the type of the item emitted by the Single + * @since (If this class graduates from "Experimental" replace this parenthetical with the release number) */ @Experimental public class Single { @@ -72,7 +77,8 @@ public class Single { * unless you specifically have a need for inheritance. * * @param f - * {@link OnExecute} to be executed when {@link #execute(SingleSubscriber)} or {@link #subscribe(Subscriber)} is called + * {@link OnExecute} to be executed when {@link #execute(SingleSubscriber)} or + * {@link #subscribe(Subscriber)} is called */ protected Single(final OnSubscribe f) { // bridge between OnSubscribe (which all Operators and Observables use) and OnExecute (for Single) @@ -109,12 +115,14 @@ private Single(final Observable.OnSubscribe f) { private static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); /** - * Returns a Single that will execute the specified function when a {@link SingleSubscriber} executes it or {@link Subscriber} subscribes to it. + * Returns a Single that will execute the specified function when a {@link SingleSubscriber} executes it or + * a {@link Subscriber} subscribes to it. *

* *

* Write the function you pass to {@code create} so that it behaves as a Single: It should invoke the - * SingleSubscriber {@link SingleSubscriber#onSuccess onSuccess} and {@link SingleSubscriber#onError onError} methods appropriately. + * SingleSubscriber {@link SingleSubscriber#onSuccess onSuccess} and/or + * {@link SingleSubscriber#onError onError} methods appropriately. *

* A well-formed Single must invoke either the SingleSubscriber's {@code onSuccess} method exactly once or * its {@code onError} method exactly once. @@ -127,7 +135,8 @@ private Single(final Observable.OnSubscribe f) { * @param * the type of the item that this Single emits * @param f - * a function that accepts an {@code SingleSubscriber}, and invokes its {@code onSuccess} or {@code onError} methods as appropriate + * a function that accepts an {@code SingleSubscriber}, and invokes its {@code onSuccess} or + * {@code onError} methods as appropriate * @return a Single that, when a {@link Subscriber} subscribes to it, will execute the specified function * @see ReactiveX operators documentation: Create */ @@ -143,17 +152,17 @@ public static interface OnSubscribe extends Action1 * In other words, this allows chaining TaskExecutors together on a Single for acting on the values within * the Single. - *

{@code - * task.map(...).filter(...).lift(new OperatorA()).lift(new OperatorB(...)).subscribe() - * }

- * If the operator you are creating is designed to act on the item emitted by a source - * Single, use {@code lift}. If your operator is designed to transform the source Single as a whole - * (for instance, by applying a particular set of existing RxJava operators to it) use {@link #compose}. + *

+ * {@code task.map(...).filter(...).lift(new OperatorA()).lift(new OperatorB(...)).subscribe() } + *

+ * If the operator you are creating is designed to act on the item emitted by a source Single, use + * {@code lift}. If your operator is designed to transform the source Single as a whole (for instance, by + * applying a particular set of existing RxJava operators to it) use {@link #compose}. *

*
Scheduler:
*
{@code lift} does not operate by default on a particular {@link Scheduler}.
@@ -198,22 +207,22 @@ public void call(Subscriber o) { } /** - * Transform an Observable by applying a particular Transformer function to it. + * Transform a Single by applying a particular Transformer function to it. *

- * This method operates on the Observable itself whereas {@link #lift} operates on the Observable's - * Subscribers or Observers. + * This method operates on the Single itself whereas {@link #lift} operates on the Single's Subscribers or + * Observers. *

- * If the operator you are creating is designed to act on the individual items emitted by a source - * Observable, use {@link #lift}. If your operator is designed to transform the source Observable as a whole - * (for instance, by applying a particular set of existing RxJava operators to it) use {@code compose}. + * If the operator you are creating is designed to act on the individual item emitted by a Single, use + * {@link #lift}. If your operator is designed to transform the source Single as a whole (for instance, by + * applying a particular set of existing RxJava operators to it) use {@code compose}. *

*
Scheduler:
*
{@code compose} does not operate by default on a particular {@link Scheduler}.
*
* * @param transformer - * implements the function that transforms the source Observable - * @return the source Observable, transformed by the transformer function + * implements the function that transforms the source Single + * @return the source Single, transformed by the transformer function * @see RxJava wiki: Implementing Your Own Operators */ @SuppressWarnings("unchecked") @@ -232,6 +241,8 @@ public static interface Transformer extends Func1, Single> { /** * + * + * @warn more complete description needed */ private static Observable toObservable(Single t) { // is this sufficient, or do I need to keep the outer Single and subscribe to it? @@ -241,8 +252,8 @@ private static Observable toObservable(Single t) { /** * INTERNAL: Used with lift and operators. * - * Converts the source {@code Single} into an {@code Single>} that emits the - * source Observable as its single emission. + * Converts the source {@code Single} into an {@code Single>} that emits an Observable + * that emits the same emission as the source Single. *

* *

@@ -250,7 +261,7 @@ private static Observable toObservable(Single t) { *
{@code nest} does not operate by default on a particular {@link Scheduler}.
*
* - * @return an Observable that emits a single item: the source Observable + * @return a Single that emits an Observable that emits the same item as the source Single * @see ReactiveX operators documentation: To */ private final Single> nest() { @@ -263,8 +274,7 @@ private final Single> nest() { */ /** - * Returns an Observable that emits the items emitted by two Tasks, one after the other, without - * interleaving them. + * Returns an Observable that emits the items emitted by two Singles, one after the other. *

* *

@@ -276,8 +286,7 @@ private final Single> nest() { * an Single to be concatenated * @param t2 * an Single to be concatenated - * @return an Observable that emits items emitted by the two source Observables, one after the other, - * without interleaving them + * @return an Observable that emits items emitted by the two source Singles, one after the other. * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2) { @@ -285,8 +294,7 @@ public final static Observable concat(Single t1, Single * *
@@ -300,8 +308,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3) { @@ -309,8 +316,7 @@ public final static Observable concat(Single t1, Single * *
@@ -326,8 +332,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4) { @@ -335,8 +340,7 @@ public final static Observable concat(Single t1, Single * *
@@ -354,8 +358,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5) { @@ -363,8 +366,7 @@ public final static Observable concat(Single t1, Single * *
@@ -384,8 +386,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { @@ -393,8 +394,7 @@ public final static Observable concat(Single t1, Single * *
@@ -416,8 +416,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { @@ -425,8 +424,7 @@ public final static Observable concat(Single t1, Single * *
@@ -450,8 +448,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { @@ -459,8 +456,7 @@ public final static Observable concat(Single t1, Single * *
@@ -486,8 +482,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { @@ -495,8 +490,8 @@ public final static Observable concat(Single t1, Single * *
@@ -505,11 +500,11 @@ public final static Observable concat(Single t1, Single * * @param exception - * the particular Throwable to pass to {@link Observer#onError onError} + * the particular Throwable to pass to {@link SingleSubscriber#onError onError} * @param - * the type of the items (ostensibly) emitted by the Observable - * @return an Observable that invokes the {@link Observer}'s {@link Observer#onError onError} method when - * the Observer subscribes to it + * the type of the item (ostensibly) emitted by the Single + * @return a Single that invokes the subscriber's {@link SingleSubscriber#onError onError} method when + * the subscriber subscribes to it * @see ReactiveX operators documentation: Throw */ public final static Single error(final Throwable exception) { @@ -524,14 +519,15 @@ public void call(SingleSubscriber te) { } /** - * Converts a {@link Future} into an Observable. + * Converts a {@link Future} into a {@code Single}. *

* *

- * You can convert any object that supports the {@link Future} interface into an Observable that emits the - * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. + * You can convert any object that supports the {@link Future} interface into a Single that emits the return + * value of the {@link Future#get} method of that object, by passing the object into the {@code from} + * method. *

- * Important note: This Observable is blocking; you cannot unsubscribe from it. + * Important note: This Single is blocking; you cannot unsubscribe from it. *

*
Scheduler:
*
{@code from} does not operate by default on a particular {@link Scheduler}.
@@ -541,8 +537,8 @@ public void call(SingleSubscriber te) { * the source {@link Future} * @param * the type of object that the {@link Future} returns, and also the type of item to be emitted by - * the resulting Observable - * @return an Observable that emits the item from the source {@link Future} + * the resulting {@code Single} + * @return a {@code Single} that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ public final static Single from(Future future) { @@ -550,14 +546,15 @@ public final static Single from(Future future) { } /** - * Converts a {@link Future} into an Observable, with a timeout on the Future. + * Converts a {@link Future} into a {@code Single}, with a timeout on the Future. *

* *

- * You can convert any object that supports the {@link Future} interface into an Observable that emits the - * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. + * You can convert any object that supports the {@link Future} interface into a {@code Single} that emits + * the return value of the {@link Future#get} method of that object, by passing the object into the + * {@code from} method. *

- * Important note: This Observable is blocking; you cannot unsubscribe from it. + * Important note: This {@code Single} is blocking; you cannot unsubscribe from it. *

*
Scheduler:
*
{@code from} does not operate by default on a particular {@link Scheduler}.
@@ -571,8 +568,8 @@ public final static Single from(Future future) { * the {@link TimeUnit} of the {@code timeout} argument * @param * the type of object that the {@link Future} returns, and also the type of item to be emitted by - * the resulting Observable - * @return an Observable that emits the item from the source {@link Future} + * the resulting {@code Single} + * @return a {@code Single} that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ public final static Single from(Future future, long timeout, TimeUnit unit) { @@ -580,12 +577,13 @@ public final static Single from(Future future, long timeout, } /** - * Converts a {@link Future}, operating on a specified {@link Scheduler}, into an Observable. + * Converts a {@link Future}, operating on a specified {@link Scheduler}, into a {@code Single}. *

* *

- * You can convert any object that supports the {@link Future} interface into an Observable that emits the - * return value of the {@link Future#get} method of that object, by passing the object into the {@code from} method. + * You can convert any object that supports the {@link Future} interface into a {@code Single} that emits + * the return value of the {@link Future#get} method of that object, by passing the object into the + * {@code from} method. *

*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
@@ -594,11 +592,12 @@ public final static Single from(Future future, long timeout, * @param future * the source {@link Future} * @param scheduler - * the {@link Scheduler} to wait for the Future on. Use a Scheduler such as {@link Schedulers#io()} that can block and wait on the Future + * the {@link Scheduler} to wait for the Future on. Use a Scheduler such as + * {@link Schedulers#io()} that can block and wait on the Future * @param * the type of object that the {@link Future} returns, and also the type of item to be emitted by - * the resulting Observable - * @return an Observable that emits the item from the source {@link Future} + * the resulting {@code Single} + * @return a {@code Single} that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ public final static Single from(Future future, Scheduler scheduler) { @@ -606,16 +605,12 @@ public final static Single from(Future future, Scheduler sch } /** - * Returns an Observable that emits a single item and then completes. + * Returns a {@code Single} that emits a specified item. *

* *

- * To convert any object into an Observable that emits that object, pass that object into the {@code just} method. - *

- * This is similar to the {@link #from(java.lang.Object[])} method, except that {@code from} will convert - * an {@link Iterable} object into an Observable that emits each of the items in the Iterable, one at a - * time, while the {@code just} method converts an Iterable into an Observable that emits the entire - * Iterable as a single item. + * To convert any object into a {@code Single} that emits that object, pass that object into the + * {@code just} method. *

*
Scheduler:
*
{@code just} does not operate by default on a particular {@link Scheduler}.
@@ -625,7 +620,7 @@ public final static Single from(Future future, Scheduler sch * the item to emit * @param * the type of that item - * @return an Observable that emits {@code value} as a single item and then completes + * @return a {@code Single} that emits {@code value} * @see ReactiveX operators documentation: Just */ public final static Single just(final T value) { @@ -641,8 +636,8 @@ public void call(SingleSubscriber te) { } /** - * Flattens a Single that emits a Single into a single Single that emits the items emitted by - * the nested Single, without any transformation. + * Flattens a {@code Single} that emits a {@code Single} into a single {@code Single} that emits the item + * emitted by the nested {@code Single}, without any transformation. *

* *

@@ -652,8 +647,9 @@ public void call(SingleSubscriber te) { *

* * @param source - * a Single that emits a Single - * @return a Single that emits the item that is the result of flattening the Single emitted by the {@code source} Single + * a {@code Single} that emits a {@code Single} + * @return a {@code Single} that emits the item that is the result of flattening the {@code Single} emitted + * by {@code source} * @see ReactiveX operators documentation: Merge */ public final static Single merge(final Single> source) { @@ -679,11 +675,11 @@ public void onError(Throwable error) { } /** - * Flattens two Observables into a single Observable, without any transformation. + * Flattens two Singles into a single Observable, without any transformation. *

* *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by * using the {@code merge} method. *

*
Scheduler:
@@ -694,7 +690,7 @@ public void onError(Throwable error) { * a Single to be merged * @param t2 * a Single to be merged - * @return an Observable that emits all of the items emitted by the source Observables + * @return an Observable that emits all of the items emitted by the source Singles * @see ReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2) { @@ -702,12 +698,12 @@ public final static Observable merge(Single t1, Single * *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code merge} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code merge} method. *

*
Scheduler:
*
{@code merge} does not operate by default on a particular {@link Scheduler}.
@@ -719,7 +715,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3) { @@ -727,12 +723,12 @@ public final static Observable merge(Single t1, Single * *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code merge} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code merge} method. *

*
Scheduler:
*
{@code merge} does not operate by default on a particular {@link Scheduler}.
@@ -746,7 +742,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4) { @@ -754,12 +750,12 @@ public final static Observable merge(Single t1, Single * *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code merge} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code merge} method. *

*
Scheduler:
*
{@code merge} does not operate by default on a particular {@link Scheduler}.
@@ -775,7 +771,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5) { @@ -783,12 +779,12 @@ public final static Observable merge(Single t1, Single * *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code merge} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code merge} method. *

*
Scheduler:
*
{@code merge} does not operate by default on a particular {@link Scheduler}.
@@ -806,7 +802,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { @@ -814,12 +810,12 @@ public final static Observable merge(Single t1, Single * *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code merge} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code merge} method. *

*
Scheduler:
*
{@code merge} does not operate by default on a particular {@link Scheduler}.
@@ -839,7 +835,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { @@ -847,12 +843,12 @@ public final static Observable merge(Single t1, Single * *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code merge} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code merge} method. *

*
Scheduler:
*
{@code merge} does not operate by default on a particular {@link Scheduler}.
@@ -874,7 +870,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { @@ -882,12 +878,12 @@ public final static Observable merge(Single t1, Single * *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code merge} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code merge} method. *

*
Scheduler:
*
{@code merge} does not operate by default on a particular {@link Scheduler}.
@@ -911,7 +907,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { @@ -919,31 +915,23 @@ public final static Observable merge(Single t1, Single * - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by {@code o1} and the first item - * emitted by {@code o2}; the second item emitted by the new Observable will be the result of the function - * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. - *

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

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results - * in an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, final Func2 zipFunction) { @@ -951,34 +939,25 @@ public final static Single zip(Single o1, Single * - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by {@code o1}, the first item - * emitted by {@code o2}, and the first item emitted by {@code o3}; the second item emitted by the new - * Observable will be the result of the function applied to the second item emitted by {@code o1}, the - * second item emitted by {@code o2}, and the second item emitted by {@code o3}; and so forth. - *

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

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param o3 - * a third source Observable + * a third source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results in - * an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Func3 zipFunction) { @@ -986,36 +965,27 @@ public final static Single zip(Single o1, Singl } /** - * Returns an Observable that emits the results of a specified combiner function applied to combinations of - * four items emitted, in sequence, by four other Observables. + * Returns an Observable that emits the results of a specified combiner function applied to four items + * emitted by four other Singles. *

* - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by {@code o1}, the first item - * emitted by {@code o2}, the first item emitted by {@code o3}, and the first item emitted by {@code 04}; - * the second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that - * emits the fewest - * items. *

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param o3 - * a third source Observable + * a third source Single * @param o4 - * a fourth source Observable + * a fourth source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results in - * an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Func4 zipFunction) { @@ -1023,38 +993,29 @@ public final static Single zip(Single o1, S } /** - * Returns an Observable that emits the results of a specified combiner function applied to combinations of - * five items emitted, in sequence, by five other Observables. + * Returns an Observable that emits the results of a specified combiner function applied to five items + * emitted by five other Singles. *

* - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by {@code o1}, the first item - * emitted by {@code o2}, the first item emitted by {@code o3}, the first item emitted by {@code o4}, and - * the first item emitted by {@code o5}; the second item emitted by the new Observable will be the result of - * the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that - * emits the fewest - * items. *

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param o3 - * a third source Observable + * a third source Single * @param o4 - * a fourth source Observable + * a fourth source Single * @param o5 - * a fifth source Observable + * a fifth source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results in - * an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Func5 zipFunction) { @@ -1062,39 +1023,31 @@ public final static Single zip(Single o } /** - * Returns an Observable that emits the results of a specified combiner function applied to combinations of - * six items emitted, in sequence, by six other Observables. + * Returns an Observable that emits the results of a specified combiner function applied to six items + * emitted by six other Singles. *

* - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by each source Observable, the - * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that - * emits the fewest - * items. *

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param o3 - * a third source Observable + * a third source Single * @param o4 - * a fourth source Observable + * a fourth source Single * @param o5 - * a fifth source Observable + * a fifth source Single * @param o6 - * a sixth source Observable + * a sixth source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results in - * an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, @@ -1103,41 +1056,33 @@ public final static Single zip(Single * - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by each source Observable, the - * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that - * emits the fewest - * items. *

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param o3 - * a third source Observable + * a third source Single * @param o4 - * a fourth source Observable + * a fourth source Single * @param o5 - * a fifth source Observable + * a fifth source Single * @param o6 - * a sixth source Observable + * a sixth source Single * @param o7 - * a seventh source Observable + * a seventh source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results in - * an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, @@ -1146,43 +1091,35 @@ public final static Single zip(Single * - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by each source Observable, the - * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that - * emits the fewest - * items. *

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param o3 - * a third source Observable + * a third source Single * @param o4 - * a fourth source Observable + * a fourth source Single * @param o5 - * a fifth source Observable + * a fifth source Single * @param o6 - * a sixth source Observable + * a sixth source Single * @param o7 - * a seventh source Observable + * a seventh source Single * @param o8 - * an eighth source Observable + * an eighth source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results in - * an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, @@ -1191,45 +1128,37 @@ public final static Single zip(Single * - *

{@code zip} applies this function in strict sequence, so the first item emitted by the new Observable - * will be the result of the function applied to the first item emitted by each source Observable, the - * second item emitted by the new Observable will be the result of the 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 {@link Observer#onNext onNext} as many times as the number of {@code onNext} invocations of the source Observable that - * emits the fewest - * items. *

*
Scheduler:
*
{@code zip} does not operate by default on a particular {@link Scheduler}.
*
* * @param o1 - * the first source Observable + * the first source Single * @param o2 - * a second source Observable + * a second source Single * @param o3 - * a third source Observable + * a third source Single * @param o4 - * a fourth source Observable + * a fourth source Single * @param o5 - * a fifth source Observable + * a fifth source Single * @param o6 - * a sixth source Observable + * a sixth source Single * @param o7 - * a seventh source Observable + * a seventh source Single * @param o8 - * an eighth source Observable + * an eighth source Single * @param o9 - * a ninth source Observable + * a ninth source Single * @param zipFunction - * a function that, when applied to an item emitted by each of the source Observables, results in - * an item that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results + * a function that, when applied to the item emitted by each of the source Singles, results in an + * item that will be emitted by the resulting Single + * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, @@ -1238,8 +1167,8 @@ public final static Single zip(Single } /** - * Returns an Observable that emits the items emitted from the current Observable, then the next, one after - * the other, without interleaving them. + * Returns an Observable that emits the item emitted by the source Single, then the item emitted by the + * specified Single. *

* *

@@ -1249,8 +1178,8 @@ public final static Single zip(Single * * @param t1 * a Single to be concatenated after the current - * @return an Observable that emits items emitted by the two source Observables, one after the other, - * without interleaving them + * @return an Observable that emits the item emitted by the source Single, followed by the item emitted by + * {@code t1} * @see ReactiveX operators documentation: Concat */ public final Observable concatWith(Single t1) { @@ -1258,9 +1187,8 @@ public final Observable concatWith(Single t1) { } /** - * Returns an Observable that emits items based on applying a function that you supply to each item emitted - * by the source Observable, where that function returns an Observable, and then merging those resulting - * Observables and emitting the results of this merger. + * Returns a Single that is based on applying a specified function to the item emitted by the source Single, + * where that function returns a Single. *

* *

@@ -1269,11 +1197,8 @@ public final Observable concatWith(Single t1) { *
* * @param func - * a function that, when applied to an item emitted by the source Observable, returns an - * Observable - * @return an Observable that emits the result of applying the transformation function to each item emitted - * by the source Observable and merging the results of the Observables obtained from this - * transformation + * a function that, when applied to the item emitted by the source Single, returns a Single + * @return the Single returned from {@code func} when applied to the item emitted by the source Single * @see ReactiveX operators documentation: FlatMap */ public final Single flatMap(final Func1> func) { @@ -1281,22 +1206,19 @@ public final Single flatMap(final Func1 - * + * *
*
Scheduler:
- *
{@code flatMap} does not operate by default on a particular {@link Scheduler}.
+ *
{@code flatMapObservable} 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 + * a function that, when applied to the item emitted by the source Single, returns an * Observable - * @return an Observable that emits the result of applying the transformation function to each item emitted - * by the source Observable and merging the results of the Observables obtained from this - * transformation + * @return the Observable returned from {@code func} when applied to the item emitted by the source Single * @see ReactiveX operators documentation: FlatMap */ public final Observable flatMapObservable(Func1> func) { @@ -1305,7 +1227,7 @@ public final Observable flatMapObservable(Func1 * *
@@ -1323,12 +1245,12 @@ public final Single map(Func1 func) { } /** - * Flattens this and another Observable into a single Observable, without any transformation. + * Flattens this and another Single into a single Observable, without any transformation. *

* *

- * You can combine items emitted by multiple Observables so that they appear as a single Observable, by - * using the {@code mergeWith} method. + * You can combine items emitted by multiple Singles so that they appear as a single Observable, by using + * the {@code mergeWith} method. *

*
Scheduler:
*
{@code mergeWith} does not operate by default on a particular {@link Scheduler}.
@@ -1336,7 +1258,7 @@ public final Single map(Func1 func) { * * @param t1 * a Single to be merged - * @return an Observable that emits all of the items emitted by the source Observables + * @return an Observable that emits all of the items emitted by the source Singles * @see ReactiveX operators documentation: Merge */ public final Observable mergeWith(Single t1) { @@ -1344,8 +1266,8 @@ public final Observable mergeWith(Single t1) { } /** - * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, - * asynchronously with an unbounded buffer. + * Modifies a Single to emit its item (or notify of its error) on a specified {@link Scheduler}, + * asynchronously. *

* *

@@ -1354,8 +1276,9 @@ public final Observable mergeWith(Single t1) { *
* * @param scheduler - * the {@link Scheduler} to notify {@link Observer}s on - * @return the source Observable modified so that its {@link Observer}s are notified on the specified {@link Scheduler} + * the {@link Scheduler} to notify subscribers on + * @return the source Single modified so that its subscribers are notified on the specified + * {@link Scheduler} * @see ReactiveX operators documentation: ObserveOn * @see RxJava Threading Examples * @see #subscribeOn @@ -1365,15 +1288,17 @@ public final Single observeOn(Scheduler scheduler) { } /** - * Instructs an Observable to emit an item (returned by a specified function) rather than invoking {@link Observer#onError onError} if it encounters an error. + * Instructs a Single to emit an item (returned by a specified function) rather than invoking + * {@link SingleSubscriber#onError onError} if it encounters an error. *

* *

- * By default, when an Observable encounters an error that prevents it from emitting the expected item to - * its {@link Observer}, the Observable invokes its Observer's {@code onError} method, and then quits - * without invoking any more of its Observer's methods. The {@code onErrorReturn} method changes this - * behavior. If you pass a function ({@code resumeFunction}) to an Observable's {@code onErrorReturn} method, if the original Observable encounters an error, instead of invoking its Observer's - * {@code onError} method, it will instead emit the return value of {@code resumeFunction}. + * By default, when a Single encounters an error that prevents it from emitting the expected item to its + * subscriber, the Single invokes its subscriber's {@link SingleSubscriber#onError} method, and then quits + * without invoking any more of its subscriber's methods. The {@code onErrorReturn} method changes this + * behavior. If you pass a function ({@code resumeFunction}) to a Single's {@code onErrorReturn} method, if + * the original Single encounters an error, instead of invoking its subscriber's + * {@link SingleSubsriber#onError} method, it will instead emit the return value of {@code resumeFunction}. *

* You can use this to prevent errors from propagating or to supply fallback data should errors be * encountered. @@ -1383,9 +1308,9 @@ public final Single observeOn(Scheduler scheduler) { *

* * @param resumeFunction - * a function that returns an item that the new Observable will emit if the source Observable - * encounters an error - * @return the original Observable with appropriately modified behavior + * a function that returns an item that the new Single will emit if the source Single encounters + * an error + * @return the original Single with appropriately modified behavior * @see ReactiveX operators documentation: Catch */ public final Single onErrorReturn(Func1 resumeFunction) { @@ -1393,7 +1318,7 @@ public final Single onErrorReturn(Func1 resumeFunctio } /** - * Subscribes to an Observable but ignore its emissions and notifications. + * Subscribes to a Single but ignore its emission or notification. *
*
Scheduler:
*
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
@@ -1401,7 +1326,7 @@ public final Single onErrorReturn(Func1 resumeFunctio * * @return a {@link Subscription} reference can request the {@link Single} stop work. * @throws OnErrorNotImplementedException - * if the Observable tries to call {@code onError} + * if the Single tries to call {@link Subscriber#onError} * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe() { @@ -1426,19 +1351,19 @@ public final void onNext(T args) { } /** - * Subscribes to an Observable and provides a callback to handle the items it emits. + * Subscribes to a Single and provides a callback to handle the item it emits. *
*
Scheduler:
*
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
*
* * @param onNext - * the {@code Action1} you have designed to accept emissions from the Observable + * the {@code Action1} you have designed to accept the emission from the Single * @return a {@link Subscription} reference can request the {@link Single} stop work. * @throws IllegalArgumentException * if {@code onNext} is null * @throws OnErrorNotImplementedException - * if the Observable tries to call {@code onError} + * if the Single tries to call {@link Subscriber#onError} * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(final Action1 onSuccess) { @@ -1467,18 +1392,18 @@ public final void onNext(T args) { } /** - * Subscribes to an Observable and provides callbacks to handle the items it emits and any error - * notification it issues. + * Subscribes to a Single and provides callbacks to handle the item it emits or any error notification it + * issues. *
*
Scheduler:
*
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
*
* * @param onNext - * the {@code Action1} you have designed to accept emissions from the Observable + * the {@code Action1} you have designed to accept the emission from the Single * @param onError * the {@code Action1} you have designed to accept any error notification from the - * Observable + * Single * @return a {@link Subscription} reference can request the {@link Single} stop work. * @see ReactiveX operators documentation: Subscribe * @throws IllegalArgumentException @@ -1514,7 +1439,7 @@ public final void onNext(T args) { } /** - * Subscribes to an Observable and invokes {@link OnSubscribe} function without any contract protection, + * Subscribes to a Single and invokes the {@link OnSubscribe} function without any contract protection, * error handling, unsubscribe, or execution hooks. *

* Use this only for implementing an {@link Operator} that requires nested subscriptions. For other @@ -1525,7 +1450,7 @@ public final void onNext(T args) { *

* * @param subscriber - * the Subscriber that will handle emissions and notifications from the Observable + * the Subscriber that will handle the emission or notification from the Single */ public final void unsafeSubscribe(Subscriber subscriber) { try { @@ -1557,19 +1482,18 @@ public final void unsafeSubscribe(Subscriber subscriber) { } /** - * Subscribes to an Single and provides a Subscriber that implements functions to handle the item the - * Single emits and any error notification it issues. + * Subscribes to a Single and provides a Subscriber that implements functions to handle the item the Single + * emits or any error notification it issues. *

* A typical implementation of {@code subscribe} does the following: *

    *
  1. It stores a reference to the Subscriber in a collection object, such as a {@code List} object.
  2. *
  3. It returns a reference to the {@link Subscription} interface. This enables Subscribers to - * unsubscribe, that is, to stop receiving items and notifications before the Observable completes, which - * also invokes the Subscriber's {@link Subscriber#onCompleted onCompleted} method.
  4. + * unsubscribe, that is, to stop receiving the item or notification before the Single completes. *

- * An {@code Single} instance is responsible for accepting all subscriptions and notifying all + * A {@code Single} instance is responsible for accepting all subscriptions and notifying all * Subscribers. Unless the documentation for a particular {@code Single} implementation indicates - * otherwise, Subscriber should make no assumptions about the order in which multiple Subscribers will + * otherwise, Subscribers should make no assumptions about the order in which multiple Subscribers will * receive their notifications. *

* For more information see the @@ -1580,7 +1504,7 @@ public final void unsafeSubscribe(Subscriber subscriber) { *

* * @param subscriber - * the {@link Subscriber} that will handle emissions and notifications from the Observable + * the {@link Subscriber} that will handle the emission or notification from the Single * @return a {@link Subscription} reference can request the {@link Single} stop work. * @throws IllegalStateException * if {@code subscribe} is unable to obtain an {@code OnSubscribe<>} function @@ -1648,19 +1572,18 @@ public final Subscription subscribe(Subscriber subscriber) { } /** - * Subscribes to an Single and provides a SingleSubscriber that implements functions to handle the item the - * Single emits and any error notification it issues. + * Subscribes to a Single and provides a {@link SingleSubscriber} that implements functions to handle the + * item the Single emits or any error notification it issues. *

* A typical implementation of {@code subscribe} does the following: *

    *
  1. It stores a reference to the Subscriber in a collection object, such as a {@code List} object.
  2. *
  3. It returns a reference to the {@link Subscription} interface. This enables Subscribers to - * unsubscribe, that is, to stop receiving items and notifications before the Observable completes, which - * also invokes the Subscriber's {@link Subscriber#onCompleted onCompleted} method.
  4. + * unsubscribe, that is, to stop receiving the item or notification before the Single completes. *

- * An {@code Single} instance is responsible for accepting all subscriptions and notifying all + * A {@code Single} instance is responsible for accepting all subscriptions and notifying all * Subscribers. Unless the documentation for a particular {@code Single} implementation indicates - * otherwise, Subscriber should make no assumptions about the order in which multiple Subscribers will + * otherwise, Subscribers should make no assumptions about the order in which multiple Subscribers will * receive their notifications. *

* For more information see the @@ -1671,7 +1594,7 @@ public final Subscription subscribe(Subscriber subscriber) { *

* * @param subscriber - * the {@link Subscriber} that will handle emissions and notifications from the Observable + * the {@link Subscriber} that will handle the emission or notification from the Single * @return a {@link Subscription} reference can request the {@link Single} stop work. * @throws IllegalStateException * if {@code subscribe} is unable to obtain an {@code OnSubscribe<>} function @@ -1708,7 +1631,7 @@ public void onNext(T t) { } /** - * Asynchronously subscribes Observers to this Observable on the specified {@link Scheduler}. + * Asynchronously subscribes subscribers to this Single on the specified {@link Scheduler}. *

* *

@@ -1718,8 +1641,7 @@ public void onNext(T t) { * * @param scheduler * the {@link Scheduler} to perform subscription actions on - * @return the source Observable modified so that its subscriptions happen on the - * specified {@link Scheduler} + * @return the source Single modified so that its subscriptions happen on the specified {@link Scheduler} * @see ReactiveX operators documentation: SubscribeOn * @see RxJava Threading Examples * @see #observeOn @@ -1729,9 +1651,9 @@ public final Single subscribeOn(Scheduler scheduler) { } /** - * Returns an Observable that mirrors the source Observable but applies a timeout policy for each emitted - * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, - * the resulting Observable terminates and notifies observers of a {@code TimeoutException}. + * 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 + * subscribers of a {@code TimeoutException}. *

* *

@@ -1740,10 +1662,10 @@ public final Single subscribeOn(Scheduler scheduler) { *
* * @param timeout - * maximum duration between emitted items before a timeout occurs + * maximum duration before the Single times out * @param timeUnit * the unit of time that applies to the {@code timeout} argument. - * @return the source Observable modified to notify observers of a {@code TimeoutException} in case of a + * @return the source Single modified to notify subscribers of a {@code TimeoutException} in case of a * timeout * @see ReactiveX operators documentation: Timeout */ @@ -1752,10 +1674,9 @@ public final Single timeout(long timeout, TimeUnit timeUnit) { } /** - * Returns an Observable that mirrors the source Observable but applies a timeout policy for each emitted - * item, where this policy is governed on a specified Scheduler. If the next item isn't emitted within the - * specified timeout duration starting from its predecessor, the resulting Observable terminates and - * notifies observers of a {@code TimeoutException}. + * Returns a Single that mirrors the source Single but applies a timeout policy for its emitted item, where + * this policy is governed on a specified Scheduler. If the item is not emitted within the specified timeout + * duration, the resulting Single terminates and notifies subscribers of a {@code TimeoutException}. *

* *

@@ -1764,12 +1685,12 @@ public final Single timeout(long timeout, TimeUnit timeUnit) { *
* * @param timeout - * maximum duration between items before a timeout occurs + * maximum duration before the Single times out * @param timeUnit * the unit of time that applies to the {@code timeout} argument * @param scheduler * the Scheduler to run the timeout timers on - * @return the source Observable modified to notify observers of a {@code TimeoutException} in case of a + * @return the source Single modified to notify subscribers of a {@code TimeoutException} in case of a * timeout * @see ReactiveX operators documentation: Timeout */ @@ -1778,9 +1699,9 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Scheduler schedu } /** - * Returns an Observable that mirrors the source Observable but applies a timeout policy for each emitted - * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, - * the resulting Observable begins instead to mirror a fallback Observable. + * 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 instead mirrors a fallback + * Single. *

* *

@@ -1789,12 +1710,12 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Scheduler schedu *
* * @param timeout - * maximum duration between items before a timeout occurs + * maximum time before a timeout occurs * @param timeUnit * the unit of time that applies to the {@code timeout} argument * @param other - * the fallback Observable to use in case of a timeout - * @return the source Observable modified to switch to the fallback Observable in case of a timeout + * the fallback Single to use in case of a timeout + * @return the source Single modified to switch to the fallback Single in case of a timeout * @see ReactiveX operators documentation: Timeout */ public final Single timeout(long timeout, TimeUnit timeUnit, Single other) { @@ -1802,9 +1723,9 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single * *
@@ -1813,15 +1734,14 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single * * @param timeout - * maximum duration between items before a timeout occurs + * maximum duration before a timeout occurs * @param timeUnit * the unit of time that applies to the {@code timeout} argument * @param other - * the Observable to use as the fallback in case of a timeout + * the Single to use as the fallback in case of a timeout * @param scheduler * the {@link Scheduler} to run the timeout timers on - * @return the source Observable modified so that it will switch to the fallback Observable in case of a - * timeout + * @return the source Single modified so that it will switch to the fallback Singlein case of a timeout * @see ReactiveX operators documentation: Timeout */ public final Single timeout(long timeout, TimeUnit timeUnit, Single other, Scheduler scheduler) { @@ -1832,8 +1752,8 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single * *
@@ -1842,14 +1762,14 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single * * @param - * the type of items emitted by the {@code other} Observable + * the type of items emitted by the {@code other} Single * @param - * the type of items emitted by the resulting Observable + * the type of items emitted by the resulting Single * @param other * the other Observable * @param zipFunction * a function that combines the pairs of items from the two Observables to generate the items to - * be emitted by the resulting Observable + * be emitted by the resulting Single * @return an Observable that pairs up values from the source Observable and the {@code other} Observable * and emits the results of {@code zipFunction} applied to these pairs * @see ReactiveX operators documentation: Zip From a658325e4b9fbe783fbacb824cca512d6cc12613 Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 11 Jun 2015 15:33:22 -0700 Subject: [PATCH 303/857] A handful more javadoc changes (misnamed @params and such) --- src/main/java/rx/Single.java | 24 ++++++++++++------------ src/main/java/rx/SingleSubscriber.java | 18 +++++++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 8edb61febb..2aad23fb9a 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -73,12 +73,12 @@ public class Single { /** * Creates a Single with a Function to execute when it is subscribed to (executed). *

- * Note: Use {@link #create(OnExecute)} to create a Single, instead of this constructor, + * Note: Use {@link #create(OnSubscribe)} to create a Single, instead of this constructor, * unless you specifically have a need for inheritance. * * @param f - * {@link OnExecute} to be executed when {@link #execute(SingleSubscriber)} or - * {@link #subscribe(Subscriber)} is called + * {@code OnExecute} to be executed when {@code execute(SingleSubscriber)} or + * {@code subscribe(Subscriber)} is called */ protected Single(final OnSubscribe f) { // bridge between OnSubscribe (which all Operators and Observables use) and OnExecute (for Single) @@ -1294,11 +1294,11 @@ public final Single observeOn(Scheduler scheduler) { * *

* By default, when a Single encounters an error that prevents it from emitting the expected item to its - * subscriber, the Single invokes its subscriber's {@link SingleSubscriber#onError} method, and then quits + * subscriber, the Single invokes its subscriber's {@link Subscriber#onError} method, and then quits * without invoking any more of its subscriber's methods. The {@code onErrorReturn} method changes this * behavior. If you pass a function ({@code resumeFunction}) to a Single's {@code onErrorReturn} method, if * the original Single encounters an error, instead of invoking its subscriber's - * {@link SingleSubsriber#onError} method, it will instead emit the return value of {@code resumeFunction}. + * {@link Subscriber#onError} method, it will instead emit the return value of {@code resumeFunction}. *

* You can use this to prevent errors from propagating or to supply fallback data should errors be * encountered. @@ -1357,7 +1357,7 @@ public final void onNext(T args) { *

{@code subscribe} does not operate by default on a particular {@link Scheduler}.
*
* - * @param onNext + * @param onSuccess * the {@code Action1} you have designed to accept the emission from the Single * @return a {@link Subscription} reference can request the {@link Single} stop work. * @throws IllegalArgumentException @@ -1399,7 +1399,7 @@ public final void onNext(T args) { *
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
*
* - * @param onNext + * @param onSuccess * the {@code Action1} you have designed to accept the emission from the Single * @param onError * the {@code Action1} you have designed to accept any error notification from the @@ -1593,17 +1593,17 @@ public final Subscription subscribe(Subscriber subscriber) { *
{@code subscribe} does not operate by default on a particular {@link Scheduler}.
*
* - * @param subscriber - * the {@link Subscriber} that will handle the emission or notification from the Single + * @param te + * the {@link SingleSubscriber} that will handle the emission or notification from the Single * @return a {@link Subscription} reference can request the {@link Single} stop work. * @throws IllegalStateException * if {@code subscribe} is unable to obtain an {@code OnSubscribe<>} function * @throws IllegalArgumentException - * if the {@link Subscriber} provided as the argument to {@code subscribe} is {@code null} + * if the {@link SingleSubscriber} provided as the argument to {@code subscribe} is {@code null} * @throws OnErrorNotImplementedException - * if the {@link Subscriber}'s {@code onError} method is null + * if the {@link SingleSubscriber}'s {@code onError} method is null * @throws RuntimeException - * if the {@link Subscriber}'s {@code onError} method itself threw a {@code Throwable} + * if the {@link SingleSubscriber}'s {@code onError} method itself threw a {@code Throwable} * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(final SingleSubscriber te) { diff --git a/src/main/java/rx/SingleSubscriber.java b/src/main/java/rx/SingleSubscriber.java index 01fa84a8c1..164933a1a3 100644 --- a/src/main/java/rx/SingleSubscriber.java +++ b/src/main/java/rx/SingleSubscriber.java @@ -21,10 +21,10 @@ /** * Provides a mechanism for receiving push-based notifications. *

- * After an SingleSubscriber calls an {@link Single}'s {@link Single#subscribe subscribe} method, the - * {@code Single} calls the SingleSubscriber's {@link #onSuccess} and {@link #onError} methods to provide notifications. - * A well-behaved {@code Single} will call an SingleSubscriber's {@link #onSuccess} method exactly once or - * the SingleSubscriber's {@link #onError} method exactly once. + * After a SingleSubscriber calls a {@link Single}'s {@link Single#subscribe subscribe} method, the + * {@code Single} calls the SingleSubscriber's {@link #onSuccess} and {@link #onError} methods to provide + * notifications. A well-behaved {@code Single} will call a SingleSubscriber's {@link #onSuccess} method exactly + * once or the SingleSubscriber's {@link #onError} method exactly once. * * @see ReactiveX documentation: Observable * @param @@ -36,9 +36,13 @@ public abstract class SingleSubscriber implements Subscription { private final SubscriptionList cs = new SubscriptionList(); /** - * Notifies the SingleSubscriber with a single item and that the {@link Single} has finished sending push-based notifications. + * Notifies the SingleSubscriber with a single item and that the {@link Single} has finished sending + * push-based notifications. *

* The {@link Single} will not call this method if it calls {@link #onError}. + * + * @param value + * the item emitted by the Single */ public abstract void onSuccess(T value); @@ -47,7 +51,7 @@ public abstract class SingleSubscriber implements Subscription { *

* If the {@link Single} calls this method, it will not thereafter call {@link #onSuccess}. * - * @param e + * @param error * the exception encountered by the Single */ public abstract void onError(Throwable error); @@ -78,4 +82,4 @@ public final void unsubscribe() { public final boolean isUnsubscribed() { return cs.isUnsubscribed(); } -} \ No newline at end of file +} From 18ff5afd380625f9157d9e9a3144baf845c09086 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 10 Jun 2015 19:22:58 +0200 Subject: [PATCH 304/857] cache now supports backpressure --- src/main/java/rx/Observable.java | 4 +- .../internal/operators/OnSubscribeCache.java | 76 --- .../rx/internal/util/CachedObservable.java | 432 ++++++++++++++++++ .../rx/internal/util/LinkedArrayList.java | 136 ++++++ .../operators/OnSubscribeCacheTest.java | 164 ------- .../internal/util/CachedObservableTest.java | 264 +++++++++++ .../rx/internal/util/LinkedArrayListTest.java | 37 ++ 7 files changed, 871 insertions(+), 242 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OnSubscribeCache.java create mode 100644 src/main/java/rx/internal/util/CachedObservable.java create mode 100644 src/main/java/rx/internal/util/LinkedArrayList.java delete mode 100644 src/test/java/rx/internal/operators/OnSubscribeCacheTest.java create mode 100644 src/test/java/rx/internal/util/CachedObservableTest.java create mode 100644 src/test/java/rx/internal/util/LinkedArrayListTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 5537bca126..2ca7ffddf0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3504,7 +3504,7 @@ public final Observable> buffer(Observable boundary, int initialC * @see ReactiveX operators documentation: Replay */ public final Observable cache() { - return create(new OnSubscribeCache(this)); + return CachedObservable.from(this); } /** @@ -3539,7 +3539,7 @@ public final Observable cache() { * @see ReactiveX operators documentation: Replay */ public final Observable cache(int capacity) { - return create(new OnSubscribeCache(this, capacity)); + return CachedObservable.from(this, capacity); } /** diff --git a/src/main/java/rx/internal/operators/OnSubscribeCache.java b/src/main/java/rx/internal/operators/OnSubscribeCache.java deleted file mode 100644 index a568fd0e0b..0000000000 --- a/src/main/java/rx/internal/operators/OnSubscribeCache.java +++ /dev/null @@ -1,76 +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.atomic.AtomicIntegerFieldUpdater; - -import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.subjects.ReplaySubject; -import rx.subjects.Subject; - -/** - * This method has similar behavior to {@link Observable#replay()} except that this auto-subscribes - * to the source Observable rather than returning a connectable Observable. - *

- * - *

- * This is useful with an Observable that you want to cache responses when you can't control the - * subscribe/unsubscribe behavior of all the Observers. - *

- * Note: You sacrifice the ability to unsubscribe from the origin when you use this operator, so be - * careful not to use this operator on Observables that emit infinite or very large numbers of - * items, as this will use up memory. - * - * @param - * the cached value type - */ -public final class OnSubscribeCache implements OnSubscribe { - protected final Observable source; - protected final Subject cache; - volatile int sourceSubscribed; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater SRC_SUBSCRIBED_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(OnSubscribeCache.class, "sourceSubscribed"); - - public OnSubscribeCache(Observable source) { - this(source, ReplaySubject. create()); - } - - public OnSubscribeCache(Observable source, int capacity) { - this(source, ReplaySubject. create(capacity)); - } - - /* accessible to tests */OnSubscribeCache(Observable source, Subject cache) { - this.source = source; - this.cache = cache; - } - - @Override - public void call(Subscriber s) { - if (SRC_SUBSCRIBED_UPDATER.compareAndSet(this, 0, 1)) { - source.subscribe(cache); - /* - * Note that we will never unsubscribe from 'source' unless we receive `onCompleted` or `onError`, - * as we want to receive and cache all of its values. - * - * This means this should never be used on an infinite or very large sequence, similar to toList(). - */ - } - cache.unsafeSubscribe(s); - } -} diff --git a/src/main/java/rx/internal/util/CachedObservable.java b/src/main/java/rx/internal/util/CachedObservable.java new file mode 100644 index 0000000000..cda4b9d277 --- /dev/null +++ b/src/main/java/rx/internal/util/CachedObservable.java @@ -0,0 +1,432 @@ +/** + * 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.util; + +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.internal.operators.NotificationLite; +import rx.subscriptions.SerialSubscription; + +/** + * An observable which auto-connects to another observable, caches the elements + * from that observable but allows terminating the connection and completing the cache. + * + * @param the source element type + */ +public final class CachedObservable extends Observable { + /** The cache and replay state. */ + private CacheState state; + + /** + * Creates a cached Observable with a default capacity hint of 16. + * @param source the source Observable to cache + * @return the CachedObservable instance + */ + public static CachedObservable from(Observable source) { + return from(source, 16); + } + + /** + * Creates a cached Observable with the given capacity hint. + * @param source the source Observable to cache + * @param capacityHint the hint for the internal buffer size + * @return the CachedObservable instance + */ + public static CachedObservable from(Observable source, int capacityHint) { + if (capacityHint < 1) { + throw new IllegalArgumentException("capacityHint > 0 required"); + } + CacheState state = new CacheState(source, capacityHint); + CachedSubscribe onSubscribe = new CachedSubscribe(state); + return new CachedObservable(onSubscribe, state); + } + + /** + * Private constructor because state needs to be shared between the Observable body and + * the onSubscribe function. + * @param onSubscribe + * @param state + */ + private CachedObservable(OnSubscribe onSubscribe, CacheState state) { + super(onSubscribe); + this.state = state; + } + + /** + * Check if this cached observable is connected to its source. + * @return true if already connected + */ + /* public */boolean isConnected() { + return state.isConnected; + } + + /** + * Returns true if there are observers subscribed to this observable. + * @return + */ + /* public */ boolean hasObservers() { + return state.producers.length != 0; + } + + /** + * Returns the number of events currently cached. + * @return + */ + /* public */ int cachedEventCount() { + return state.size(); + } + + /** + * Contains the active child producers and the values to replay. + * + * @param + */ + static final class CacheState extends LinkedArrayList implements Observer { + /** The source observable to connect to. */ + final Observable source; + /** Holds onto the subscriber connected to source. */ + final SerialSubscription connection; + /** Guarded by connection (not this). */ + volatile ReplayProducer[] producers; + /** The default empty array of producers. */ + static final ReplayProducer[] EMPTY = new ReplayProducer[0]; + + final NotificationLite nl; + + /** Set to true after connection. */ + volatile boolean isConnected; + /** + * Indicates that the source has completed emitting values or the + * Observable was forcefully terminated. + */ + boolean sourceDone; + + public CacheState(Observable source, int capacityHint) { + super(capacityHint); + this.source = source; + this.producers = EMPTY; + this.nl = NotificationLite.instance(); + this.connection = new SerialSubscription(); + } + /** + * Adds a ReplayProducer to the producers array atomically. + * @param p + */ + public void addProducer(ReplayProducer p) { + // guarding by connection to save on allocating another object + // thus there are two distinct locks guarding the value-addition and child come-and-go + synchronized (connection) { + ReplayProducer[] a = producers; + int n = a.length; + ReplayProducer[] b = new ReplayProducer[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = p; + producers = b; + } + } + /** + * Removes the ReplayProducer (if present) from the producers array atomically. + * @param p + */ + public void removeProducer(ReplayProducer p) { + synchronized (connection) { + ReplayProducer[] a = producers; + int n = a.length; + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i].equals(p)) { + j = i; + break; + } + } + if (j < 0) { + return; + } + if (n == 1) { + producers = EMPTY; + return; + } + ReplayProducer[] b = new ReplayProducer[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + producers = b; + } + } + /** + * Connects the cache to the source. + * Make sure this is called only once. + */ + public void connect() { + connection.set(source.subscribe(this)); + isConnected = true; + } + @Override + public void onNext(T t) { + Object o = nl.next(t); + synchronized (this) { + if (!sourceDone) { + add(o); + } else { + return; + } + } + dispatch(); + } + @Override + public void onError(Throwable e) { + Object o = nl.error(e); + synchronized (this) { + if (!sourceDone) { + sourceDone = true; + add(o); + } else { + return; + } + } + connection.unsubscribe(); + dispatch(); + } + @Override + public void onCompleted() { + Object o = nl.completed(); + synchronized (this) { + if (!sourceDone) { + sourceDone = true; + add(o); + } else { + return; + } + } + connection.unsubscribe(); + dispatch(); + } + /** + * Signals all known children there is work to do. + */ + void dispatch() { + ReplayProducer[] a = producers; + for (ReplayProducer rp : a) { + rp.replay(); + } + } + } + + /** + * Manages the subscription of child subscribers by setting up a replay producer and + * performs auto-connection of the very first subscription. + * @param the value type emitted + */ + static final class CachedSubscribe extends AtomicBoolean implements OnSubscribe { + /** */ + private static final long serialVersionUID = -2817751667698696782L; + final CacheState state; + public CachedSubscribe(CacheState state) { + this.state = state; + } + @Override + public void call(Subscriber t) { + // we can connect first because we replay everything anyway + ReplayProducer rp = new ReplayProducer(t, state); + state.addProducer(rp); + + t.add(rp); + t.setProducer(rp); + + // we ensure a single connection here to save an instance field of AtomicBoolean in state. + if (!get() && compareAndSet(false, true)) { + state.connect(); + } + + // no need to call rp.replay() here because the very first request will trigger it anyway + } + } + + /** + * Keeps track of the current request amount and the replay position for a child Subscriber. + * + * @param + */ + static final class ReplayProducer extends AtomicLong implements Producer, Subscription { + /** */ + private static final long serialVersionUID = -2557562030197141021L; + /** The actual child subscriber. */ + final Subscriber child; + /** The cache state object. */ + final CacheState state; + + /** + * Contains the reference to the buffer segment in replay. + * Accessed after reading state.size() and when emitting == true. + */ + Object[] currentBuffer; + /** + * Contains the index into the currentBuffer where the next value is expected. + * Accessed after reading state.size() and when emitting == true. + */ + int currentIndexInBuffer; + /** + * Contains the absolute index up until the values have been replayed so far. + */ + int index; + + /** Indicates there is a replay going on; guarded by this. */ + boolean emitting; + /** Indicates there were some state changes/replay attempts; guarded by this. */ + boolean missed; + + public ReplayProducer(Subscriber child, CacheState state) { + this.child = child; + this.state = state; + } + @Override + public void request(long n) { + for (;;) { + long r = get(); + if (r < 0) { + return; + } + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + if (compareAndSet(r, u)) { + replay(); + return; + } + } + } + /** + * Updates the request count to reflect values have been produced. + * @param n + * @return + */ + public long produced(long n) { + return addAndGet(-n); + } + + @Override + public boolean isUnsubscribed() { + return get() < 0; + } + @Override + public void unsubscribe() { + long r = get(); + if (r >= 0) { + r = getAndSet(-1L); // unsubscribed state is negative + if (r >= 0) { + state.removeProducer(this); + } + } + } + + /** + * Continue replaying available values if there are requests for them. + */ + public void replay() { + // make sure there is only a single thread emitting + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + } + boolean skipFinal = false; + try { + final NotificationLite nl = state.nl; + final Subscriber child = this.child; + + for (;;) { + + long r = get(); + // read the size, if it is non-zero, we can safely read the head and + // read values up to the given absolute index + int s = state.size(); + if (s != 0) { + Object[] b = currentBuffer; + + // latch onto the very first buffer now that it is available. + if (b == null) { + b = state.head(); + currentBuffer = b; + } + final int n = b.length - 1; + int j = index; + int k = currentIndexInBuffer; + // eagerly emit any terminal event + if (r == 0) { + Object o = b[k]; + if (nl.isCompleted(o)) { + child.onCompleted(); + skipFinal = true; + unsubscribe(); + return; + } else + if (nl.isError(o)) { + child.onError(nl.getError(o)); + skipFinal = true; + unsubscribe(); + return; + } + } else + if (r > 0) { + int valuesProduced = 0; + + while (j < s && r > 0 && !child.isUnsubscribed()) { + if (k == n) { + b = (Object[])b[n]; + k = 0; + } + Object o = b[k]; + + if (nl.accept(child, o)) { + skipFinal = true; + unsubscribe(); + return; + } + + k++; + j++; + r--; + valuesProduced++; + } + + index = j; + currentIndexInBuffer = k; + currentBuffer = b; + produced(valuesProduced); + } + } + + synchronized (this) { + if (!missed) { + emitting = false; + skipFinal = true; + return; + } + missed = false; + } + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + } +} diff --git a/src/main/java/rx/internal/util/LinkedArrayList.java b/src/main/java/rx/internal/util/LinkedArrayList.java new file mode 100644 index 0000000000..57a1289640 --- /dev/null +++ b/src/main/java/rx/internal/util/LinkedArrayList.java @@ -0,0 +1,136 @@ +/** + * 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.util; + +import java.util.*; + +/** + * A list implementation which combines an ArrayList with a LinkedList to + * avoid copying values when the capacity needs to be increased. + *

+ * The class is non final to allow embedding it directly and thus saving on object allocation. + */ +public class LinkedArrayList { + /** The capacity of each array segment. */ + final int capacityHint; + /** + * Contains the head of the linked array list if not null. The + * length is always capacityHint + 1 and the last element is an Object[] pointing + * to the next element of the linked array list. + */ + Object[] head; + /** The tail array where new elements will be added. */ + Object[] tail; + /** + * The total size of the list; written after elements have been added (release) and + * and when read, the value indicates how many elements can be safely read (acquire). + */ + volatile int size; + /** The next available slot in the current tail. */ + int indexInTail; + /** + * Constructor with the capacity hint of each array segment. + * @param capacityHint + */ + public LinkedArrayList(int capacityHint) { + this.capacityHint = capacityHint; + } + /** + * Adds a new element to this list. + * @param o the object to add, nulls are accepted + */ + public void add(Object o) { + // if no value yet, create the first array + if (size == 0) { + head = new Object[capacityHint + 1]; + tail = head; + head[0] = o; + indexInTail = 1; + size = 1; + } else + // if the tail is full, create a new tail and link + if (indexInTail == capacityHint) { + Object[] t = new Object[capacityHint + 1]; + t[0] = o; + tail[capacityHint] = t; + tail = t; + indexInTail = 1; + size++; + } else { + tail[indexInTail] = o; + indexInTail++; + size++; + } + } + /** + * Returns the head buffer segment or null if the list is empty. + * @return + */ + public Object[] head() { + return head; + } + /** + * Returns the tail buffer segment or null if the list is empty. + * @return + */ + public Object[] tail() { + return tail; + } + /** + * Returns the total size of the list. + * @return + */ + public int size() { + return size; + } + /** + * Returns the index of the next slot in the tail buffer segment. + * @return + */ + public int indexInTail() { + return indexInTail; + } + /** + * Returns the capacity hint that indicates the capacity of each buffer segment. + * @return + */ + public int capacityHint() { + return capacityHint; + } + /* Test support */List toList() { + final int cap = capacityHint; + final int s = size; + final List list = new ArrayList(s + 1); + + Object[] h = head(); + int j = 0; + int k = 0; + while (j < s) { + list.add(h[k]); + j++; + if (++k == cap) { + k = 0; + h = (Object[])h[cap]; + } + } + + return list; + } + @Override + public String toString() { + return toList().toString(); + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java deleted file mode 100644 index 0d74cd878b..0000000000 --- a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java +++ /dev/null @@ -1,164 +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 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 java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import rx.Observable; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.functions.Func2; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.subjects.AsyncSubject; -import rx.subjects.BehaviorSubject; -import rx.subjects.PublishSubject; -import rx.subjects.ReplaySubject; -import rx.subjects.Subject; - -public class OnSubscribeCacheTest { - - @Test - public void testCache() throws InterruptedException { - final AtomicInteger counter = new AtomicInteger(); - Observable o = Observable.create(new Observable.OnSubscribe() { - - @Override - public void call(final Subscriber observer) { - new Thread(new Runnable() { - - @Override - public void run() { - counter.incrementAndGet(); - System.out.println("published observable being executed"); - observer.onNext("one"); - observer.onCompleted(); - } - }).start(); - } - }).cache(); - - // we then expect the following 2 subscriptions to get that same value - final CountDownLatch latch = new CountDownLatch(2); - - // subscribe once - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - // subscribe again - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - if (!latch.await(1000, TimeUnit.MILLISECONDS)) { - fail("subscriptions did not receive values"); - } - assertEquals(1, counter.get()); - } - - private void testWithCustomSubjectAndRepeat(Subject subject, Integer... expected) { - Observable source0 = Observable.just(1, 2, 3) - .subscribeOn(Schedulers.io()) - .flatMap(new Func1>() { - @Override - public Observable call(final Integer i) { - return Observable.timer(i * 20, TimeUnit.MILLISECONDS).map(new Func1() { - @Override - public Integer call(Long t1) { - return i; - } - }); - } - }); - - Observable source1 = Observable.create(new OnSubscribeCache(source0, subject)); - - Observable source2 = source1 - .repeat(4) - .zipWith(Observable.interval(0, 10, TimeUnit.MILLISECONDS, Schedulers.newThread()), new Func2() { - @Override - public Integer call(Integer t1, Long t2) { - return t1; - } - - }); - TestSubscriber ts = new TestSubscriber(); - source2.subscribe(ts); - - ts.awaitTerminalEvent(); - ts.assertNoErrors(); - System.out.println(ts.getOnNextEvents()); - ts.assertReceivedOnNext(Arrays.asList(expected)); - } - - @Test(timeout = 10000) - public void testWithAsyncSubjectAndRepeat() { - testWithCustomSubjectAndRepeat(AsyncSubject. create(), 3, 3, 3, 3); - } - - @Test(timeout = 10000) - public void testWithBehaviorSubjectAndRepeat() { - // BehaviorSubject just completes when repeated - testWithCustomSubjectAndRepeat(BehaviorSubject.create(0), 0, 1, 2, 3); - } - - @Test(timeout = 10000) - public void testWithPublishSubjectAndRepeat() { - // PublishSubject just completes when repeated - testWithCustomSubjectAndRepeat(PublishSubject. create(), 1, 2, 3); - } - - @Test - public void testWithReplaySubjectAndRepeat() { - testWithCustomSubjectAndRepeat(ReplaySubject. create(), 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3); - } - - @Test - public void testUnsubscribeSource() { - Action0 unsubscribe = mock(Action0.class); - Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); - o.subscribe(); - o.subscribe(); - o.subscribe(); - verify(unsubscribe, times(1)).call(); - } -} diff --git a/src/test/java/rx/internal/util/CachedObservableTest.java b/src/test/java/rx/internal/util/CachedObservableTest.java new file mode 100644 index 0000000000..c14018390f --- /dev/null +++ b/src/test/java/rx/internal/util/CachedObservableTest.java @@ -0,0 +1,264 @@ +/** + * 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.util; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; + +import rx.*; +import rx.Observable.OnSubscribe; +import rx.Observable; +import rx.exceptions.TestException; +import rx.functions.*; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; + +public class CachedObservableTest { + @Test + public void testColdReplayNoBackpressure() { + CachedObservable source = CachedObservable.from(Observable.range(0, 1000)); + + assertFalse("Source is connected!", source.isConnected()); + + TestSubscriber ts = new TestSubscriber(); + + source.subscribe(ts); + + assertTrue("Source is not connected!", source.isConnected()); + assertFalse("Subscribers retained!", source.hasObservers()); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + List onNextEvents = ts.getOnNextEvents(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + @Test + public void testColdReplayBackpressure() { + CachedObservable source = CachedObservable.from(Observable.range(0, 1000)); + + assertFalse("Source is connected!", source.isConnected()); + + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(10); + + source.subscribe(ts); + + assertTrue("Source is not connected!", source.isConnected()); + assertTrue("Subscribers not retained!", source.hasObservers()); + + ts.assertNoErrors(); + assertTrue(ts.getOnCompletedEvents().isEmpty()); + List onNextEvents = ts.getOnNextEvents(); + assertEquals(10, onNextEvents.size()); + + for (int i = 0; i < 10; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + + ts.unsubscribe(); + assertFalse("Subscribers retained!", source.hasObservers()); + } + + @Test + public void testCache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable o = Observable.create(new Observable.OnSubscribe() { + + @Override + public void call(final Subscriber observer) { + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + observer.onNext("one"); + observer.onCompleted(); + } + }).start(); + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void testUnsubscribeSource() { + Action0 unsubscribe = mock(Action0.class); + Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, times(1)).call(); + } + + @Test + public void testTake() { + TestSubscriber ts = new TestSubscriber(); + + CachedObservable cached = CachedObservable.from(Observable.range(1, 100)); + cached.take(10).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + ts.assertUnsubscribed(); + assertFalse(cached.hasObservers()); + } + + @Test + public void testAsync() { + Observable source = Observable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestSubscriber ts1 = new TestSubscriber(); + + CachedObservable cached = CachedObservable.from(source); + + cached.observeOn(Schedulers.computation()).subscribe(ts1); + + ts1.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts1.assertNoErrors(); + ts1.assertTerminalEvent(); + assertEquals(10000, ts1.getOnNextEvents().size()); + + TestSubscriber ts2 = new TestSubscriber(); + cached.observeOn(Schedulers.computation()).subscribe(ts2); + + ts2.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts2.assertNoErrors(); + ts2.assertTerminalEvent(); + assertEquals(10000, ts2.getOnNextEvents().size()); + } + } + @Test + public void testAsyncComeAndGo() { + Observable source = Observable.timer(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + CachedObservable cached = CachedObservable.from(source); + + Observable output = cached.observeOn(Schedulers.computation()); + + List> list = new ArrayList>(100); + for (int i = 0; i < 100; i++) { + TestSubscriber ts = new TestSubscriber(); + list.add(ts); + output.skip(i * 10).take(10).subscribe(ts); + } + + List expected = new ArrayList(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestSubscriber ts : list) { + ts.awaitTerminalEvent(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + ts.assertReceivedOnNext(expected); + + j++; + } + } + + @Test + public void testNoMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Observable firehose = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onCompleted(); + } + }); + + TestSubscriber ts = new TestSubscriber(); + firehose.cache().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); + + ts.awaitTerminalEvent(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertEquals(100, ts.getOnNextEvents().size()); + } + + @Test + public void testValuesAndThenError() { + Observable source = Observable.range(1, 10) + .concatWith(Observable.error(new TestException())) + .cache(); + + + TestSubscriber ts = new TestSubscriber(); + source.subscribe(ts); + + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + Assert.assertEquals(1, ts.getOnErrorEvents().size()); + + TestSubscriber ts2 = new TestSubscriber(); + source.subscribe(ts2); + + ts2.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + Assert.assertTrue(ts2.getOnCompletedEvents().isEmpty()); + Assert.assertEquals(1, ts2.getOnErrorEvents().size()); + } +} diff --git a/src/test/java/rx/internal/util/LinkedArrayListTest.java b/src/test/java/rx/internal/util/LinkedArrayListTest.java new file mode 100644 index 0000000000..af7e167c19 --- /dev/null +++ b/src/test/java/rx/internal/util/LinkedArrayListTest.java @@ -0,0 +1,37 @@ +/** + * 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.util; + +import java.util.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +public class LinkedArrayListTest { + @Test + public void testAdd() { + LinkedArrayList list = new LinkedArrayList(16); + + List expected = new ArrayList(32); + for (int i = 0; i < 32; i++) { + list.add(i); + expected.add(i); + } + + assertEquals(expected, list.toList()); + assertEquals(32, list.size()); + } +} From b3c611c39cd133e7eae5e9e66b07ba8c9ff92656 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 17 Jun 2015 14:51:57 +0200 Subject: [PATCH 305/857] ConnectableObservable autoConnect operator --- src/main/java/rx/Observable.java | 6 +- .../operators/OnSubscribeAutoConnect.java | 55 ++++++ .../rx/observables/ConnectableObservable.java | 61 +++++- .../ConnectableObservableTest.java | 175 ++++++++++++++++++ 4 files changed, 289 insertions(+), 8 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java create mode 100644 src/test/java/rx/observables/ConnectableObservableTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 2ca7ffddf0..7168467775 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3533,13 +3533,13 @@ public final Observable cache() { *
{@code cache} does not operate by default on a particular {@link Scheduler}.
* * - * @param capacity hint for number of items to cache (for optimizing underlying data structure) + * @param capacityHint hint for number of items to cache (for optimizing underlying data structure) * @return an Observable that, when first subscribed to, caches all of its items and notifications for the * benefit of subsequent subscribers * @see ReactiveX operators documentation: Replay */ - public final Observable cache(int capacity) { - return CachedObservable.from(this, capacity); + public final Observable cache(int capacityHint) { + return CachedObservable.from(this, capacityHint); } /** diff --git a/src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java b/src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java new file mode 100644 index 0000000000..c664717332 --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java @@ -0,0 +1,55 @@ +/** + * 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.atomic.AtomicInteger; + +import rx.Observable.OnSubscribe; +import rx.*; +import rx.functions.Action1; +import rx.observables.ConnectableObservable; + +/** + * Wraps a ConnectableObservable and calls its connect() method once + * the specified number of Subscribers have subscribed. + * + * @param the value type of the chain + */ +public final class OnSubscribeAutoConnect implements OnSubscribe { + final ConnectableObservable source; + final int numberOfSubscribers; + final Action1 connection; + final AtomicInteger clients; + + public OnSubscribeAutoConnect(ConnectableObservable source, + int numberOfSubscribers, + Action1 connection) { + if (numberOfSubscribers <= 0) { + throw new IllegalArgumentException("numberOfSubscribers > 0 required"); + } + this.source = source; + this.numberOfSubscribers = numberOfSubscribers; + this.connection = connection; + this.clients = new AtomicInteger(); + } + @Override + public void call(Subscriber child) { + source.unsafeSubscribe(child); + if (clients.incrementAndGet() == numberOfSubscribers) { + source.connect(connection); + } + } +} diff --git a/src/main/java/rx/observables/ConnectableObservable.java b/src/main/java/rx/observables/ConnectableObservable.java index a3c80ef1b4..868c2d3071 100644 --- a/src/main/java/rx/observables/ConnectableObservable.java +++ b/src/main/java/rx/observables/ConnectableObservable.java @@ -15,11 +15,10 @@ */ package rx.observables; -import rx.Observable; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action1; -import rx.internal.operators.OnSubscribeRefCount; +import rx.*; +import rx.annotations.Experimental; +import rx.functions.*; +import rx.internal.operators.*; /** * A {@code ConnectableObservable} resembles an ordinary {@link Observable}, except that it does not begin @@ -80,4 +79,56 @@ public void call(Subscription t1) { public Observable refCount() { return create(new OnSubscribeRefCount(this)); } + + /** + * Returns an Observable that automatically connects to this ConnectableObservable + * when the first Subscriber subscribes. + * + * @return an Observable that automatically connects to this ConnectableObservable + * when the first Subscriber subscribes + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public Observable autoConnect() { + return autoConnect(1); + } + /** + * Returns an Observable that automatically connects to this ConnectableObservable + * when the specified number of Subscribers subscribe to it. + * + * @param numberOfSubscribers the number of subscribers to await before calling connect + * on the ConnectableObservable. A non-positive value indicates + * an immediate connection. + * @return an Observable that automatically connects to this ConnectableObservable + * when the specified number of Subscribers subscribe to it + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public Observable autoConnect(int numberOfSubscribers) { + return autoConnect(numberOfSubscribers, Actions.empty()); + } + + /** + * Returns an Observable that automatically connects to this ConnectableObservable + * when the specified number of Subscribers subscribe to it and calls the + * specified callback with the Subscription associated with the established connection. + * + * @param numberOfSubscribers the number of subscribers to await before calling connect + * on the ConnectableObservable. A non-positive value indicates + * an immediate connection. + * @param connection the callback Action1 that will receive the Subscription representing the + * established connection + * @return an Observable that automatically connects to this ConnectableObservable + * when the specified number of Subscribers subscribe to it and calls the + * specified callback with the Subscription associated with the established connection + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + */ + @Experimental + public Observable autoConnect(int numberOfSubscribers, Action1 connection) { + if (numberOfSubscribers <= 0) { + this.connect(connection); + return this; + } + return create(new OnSubscribeAutoConnect(this, numberOfSubscribers, connection)); + } } diff --git a/src/test/java/rx/observables/ConnectableObservableTest.java b/src/test/java/rx/observables/ConnectableObservableTest.java new file mode 100644 index 0000000000..419c694bb0 --- /dev/null +++ b/src/test/java/rx/observables/ConnectableObservableTest.java @@ -0,0 +1,175 @@ +/** + * 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.observables; + +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import rx.*; +import rx.functions.*; +import rx.observers.TestSubscriber; + +public class ConnectableObservableTest { + @Test + public void testAutoConnect() { + final AtomicInteger run = new AtomicInteger(); + + ConnectableObservable co = Observable.defer(new Func0>() { + @Override + public Observable call() { + return Observable.just(run.incrementAndGet()); + } + }).publish(); + + Observable source = co.autoConnect(); + + Assert.assertEquals(0, run.get()); + + TestSubscriber ts1 = TestSubscriber.create(); + source.subscribe(ts1); + + ts1.assertCompleted(); + ts1.assertNoErrors(); + ts1.assertValue(1); + + Assert.assertEquals(1, run.get()); + + TestSubscriber ts2 = TestSubscriber.create(); + source.subscribe(ts2); + + ts2.assertNotCompleted(); + ts2.assertNoErrors(); + ts2.assertNoValues(); + + Assert.assertEquals(1, run.get()); + } + @Test + public void testAutoConnect0() { + final AtomicInteger run = new AtomicInteger(); + + ConnectableObservable co = Observable.defer(new Func0>() { + @Override + public Observable call() { + return Observable.just(run.incrementAndGet()); + } + }).publish(); + + Observable source = co.autoConnect(0); + + Assert.assertEquals(1, run.get()); + + TestSubscriber ts1 = TestSubscriber.create(); + source.subscribe(ts1); + + ts1.assertNotCompleted(); + ts1.assertNoErrors(); + ts1.assertNoValues(); + + Assert.assertEquals(1, run.get()); + + TestSubscriber ts2 = TestSubscriber.create(); + source.subscribe(ts2); + + ts2.assertNotCompleted(); + ts2.assertNoErrors(); + ts2.assertNoValues(); + + Assert.assertEquals(1, run.get()); + } + @Test + public void testAutoConnect2() { + final AtomicInteger run = new AtomicInteger(); + + ConnectableObservable co = Observable.defer(new Func0>() { + @Override + public Observable call() { + return Observable.just(run.incrementAndGet()); + } + }).publish(); + + Observable source = co.autoConnect(2); + + Assert.assertEquals(0, run.get()); + + TestSubscriber ts1 = TestSubscriber.create(); + source.subscribe(ts1); + + ts1.assertNotCompleted(); + ts1.assertNoErrors(); + ts1.assertNoValues(); + + Assert.assertEquals(0, run.get()); + + TestSubscriber ts2 = TestSubscriber.create(); + source.subscribe(ts2); + + Assert.assertEquals(1, run.get()); + + ts1.assertCompleted(); + ts1.assertNoErrors(); + ts1.assertValue(1); + + ts2.assertCompleted(); + ts2.assertNoErrors(); + ts2.assertValue(1); + + } + + @Test + public void testAutoConnectUnsubscribe() { + final AtomicInteger run = new AtomicInteger(); + + ConnectableObservable co = Observable.defer(new Func0>() { + @Override + public Observable call() { + return Observable.range(run.incrementAndGet(), 10); + } + }).publish(); + + final AtomicReference conn = new AtomicReference(); + + Observable source = co.autoConnect(1, new Action1() { + @Override + public void call(Subscription t) { + conn.set(t); + } + }); + + Assert.assertEquals(0, run.get()); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + super.onNext(t); + Subscription s = conn.get(); + if (s != null) { + s.unsubscribe(); + } else { + onError(new NullPointerException("No connection reference")); + } + } + }; + + source.subscribe(ts); + + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertValue(1); + + Assert.assertTrue("Connection not unsubscribed?", conn.get().isUnsubscribed()); + } +} From ec3d522c826c3135b9f5e3a9bb34f62756ec95cc Mon Sep 17 00:00:00 2001 From: David Gross Date: Wed, 17 Jun 2015 10:32:44 -0700 Subject: [PATCH 306/857] If cache() now supports backpressure, correct javadocs to indicate this. --- src/main/java/rx/Observable.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 2ca7ffddf0..c576ea59f9 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3493,8 +3493,7 @@ public final Observable> buffer(Observable boundary, int initialC * of items that will use up memory. *
*
Backpressure Support:
- *
This operator does not support upstream backpressure as it is purposefully requesting and caching - * everything emitted.
+ *
This operator supports backpressure.
*
Scheduler:
*
{@code cache} does not operate by default on a particular {@link Scheduler}.
*
@@ -3527,8 +3526,7 @@ public final Observable cache() { * of items that will use up memory. *
*
Backpressure Support:
- *
This operator does not support upstream backpressure as it is purposefully requesting and caching - * everything emitted.
+ *
This operator supports backpressure.
*
Scheduler:
*
{@code cache} does not operate by default on a particular {@link Scheduler}.
*
From 6f259eb229470099173b20b236e55435597b1e3f Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 18 Jun 2015 11:09:53 +0200 Subject: [PATCH 307/857] Delay: error cut ahead was not properly serialized --- .../rx/internal/operators/OperatorDelay.java | 26 +++++++++++++++---- .../internal/operators/OperatorDelayTest.java | 23 ++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorDelay.java b/src/main/java/rx/internal/operators/OperatorDelay.java index 48b8454dc8..00ab5d1b49 100644 --- a/src/main/java/rx/internal/operators/OperatorDelay.java +++ b/src/main/java/rx/internal/operators/OperatorDelay.java @@ -49,22 +49,36 @@ public Subscriber call(final Subscriber child) { final Worker worker = scheduler.createWorker(); child.add(worker); return new Subscriber(child) { - + // indicates an error cut ahead + // accessed from the worker thread only + boolean done; @Override public void onCompleted() { worker.schedule(new Action0() { @Override public void call() { - child.onCompleted(); + if (!done) { + done = true; + child.onCompleted(); + } } }, delay, unit); } @Override - public void onError(Throwable e) { - child.onError(e); + public void onError(final Throwable e) { + worker.schedule(new Action0() { + @Override + public void call() { + if (!done) { + done = true; + child.onError(e); + worker.unsubscribe(); + } + } + }); } @Override @@ -73,7 +87,9 @@ public void onNext(final T t) { @Override public void call() { - child.onNext(t); + if (!done) { + child.onNext(t); + } } }, delay, unit); diff --git a/src/test/java/rx/internal/operators/OperatorDelayTest.java b/src/test/java/rx/internal/operators/OperatorDelayTest.java index 9f80f0dc73..e4db021eaf 100644 --- a/src/test/java/rx/internal/operators/OperatorDelayTest.java +++ b/src/test/java/rx/internal/operators/OperatorDelayTest.java @@ -798,4 +798,27 @@ public Integer call(Integer t) { ts.assertNoErrors(); assertEquals(RxRingBuffer.SIZE * 2, ts.getOnNextEvents().size()); } + + @Test + public void testErrorRunsBeforeOnNext() { + TestScheduler test = Schedulers.test(); + + PublishSubject ps = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + ps.delay(1, TimeUnit.SECONDS, test).subscribe(ts); + + ps.onNext(1); + + test.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + ps.onError(new TestException()); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } } From 9619bb086ecad2168775768ce7d5d1557278fe34 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 18 Jun 2015 11:31:39 +0200 Subject: [PATCH 308/857] Replaced tabs with spaces for good. --- .../util/PaddedAtomicIntegerBase.java | 12 ++-- src/test/java/rx/ConcatTests.java | 42 ++++++------- .../operators/OperatorBufferTest.java | 62 +++++++++---------- .../operators/OperatorOnErrorReturnTest.java | 10 +-- 4 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java b/src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java index 3ca9e05c19..afa67e4b81 100644 --- a/src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java +++ b/src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java @@ -59,22 +59,22 @@ public final int getAndSet(int newValue) { } public final int getAndAdd(int delta) { - return updater.getAndAdd(this, delta); + return updater.getAndAdd(this, delta); } public final int incrementAndGet() { - return updater.incrementAndGet(this); + return updater.incrementAndGet(this); } public final int decrementAndGet() { - return updater.decrementAndGet(this); + return updater.decrementAndGet(this); } public final int getAndIncrement() { - return updater.getAndIncrement(this); + return updater.getAndIncrement(this); } public final int getAndDecrement() { - return updater.getAndDecrement(this); + return updater.getAndDecrement(this); } public final int addAndGet(int delta) { - return updater.addAndGet(this, delta); + return updater.addAndGet(this, delta); } @Override diff --git a/src/test/java/rx/ConcatTests.java b/src/test/java/rx/ConcatTests.java index 8e15164d86..c231b59109 100644 --- a/src/test/java/rx/ConcatTests.java +++ b/src/test/java/rx/ConcatTests.java @@ -81,11 +81,11 @@ public void testConcatWithIterableOfObservable() { @Test public void testConcatCovariance() { - HorrorMovie horrorMovie1 = new HorrorMovie(); - Movie movie = new Movie(); - Media media = new Media(); - HorrorMovie horrorMovie2 = new HorrorMovie(); - + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + Observable o1 = Observable. just(horrorMovie1, movie); Observable o2 = Observable.just(media, horrorMovie2); @@ -102,12 +102,12 @@ public void testConcatCovariance() { @Test public void testConcatCovariance2() { - HorrorMovie horrorMovie1 = new HorrorMovie(); - Movie movie = new Movie(); - Media media1 = new Media(); - Media media2 = new Media(); - HorrorMovie horrorMovie2 = new HorrorMovie(); - + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media1 = new Media(); + Media media2 = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + Observable o1 = Observable.just(horrorMovie1, movie, media1); Observable o2 = Observable.just(media2, horrorMovie2); @@ -125,11 +125,11 @@ public void testConcatCovariance2() { @Test public void testConcatCovariance3() { - HorrorMovie horrorMovie1 = new HorrorMovie(); - Movie movie = new Movie(); - Media media = new Media(); - HorrorMovie horrorMovie2 = new HorrorMovie(); - + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + Observable o1 = Observable.just(horrorMovie1, movie); Observable o2 = Observable.just(media, horrorMovie2); @@ -144,11 +144,11 @@ public void testConcatCovariance3() { @Test public void testConcatCovariance4() { - final HorrorMovie horrorMovie1 = new HorrorMovie(); - final Movie movie = new Movie(); - Media media = new Media(); - HorrorMovie horrorMovie2 = new HorrorMovie(); - + final HorrorMovie horrorMovie1 = new HorrorMovie(); + final Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + Observable o1 = Observable.create(new OnSubscribe() { @Override diff --git a/src/test/java/rx/internal/operators/OperatorBufferTest.java b/src/test/java/rx/internal/operators/OperatorBufferTest.java index d05c151ee2..75cddb8ad1 100644 --- a/src/test/java/rx/internal/operators/OperatorBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorBufferTest.java @@ -983,36 +983,36 @@ public void onNext(List t) { } @Test(timeout = 3000) public void testBufferWithTimeDoesntUnsubscribeDownstream() throws InterruptedException { - @SuppressWarnings("unchecked") - final Observer o = mock(Observer.class); - - - final CountDownLatch cdl = new CountDownLatch(1); - Subscriber s = new Subscriber() { - @Override - public void onNext(Object t) { - o.onNext(t); - } - @Override - public void onError(Throwable e) { - o.onError(e); - cdl.countDown(); - } - @Override - public void onCompleted() { - o.onCompleted(); - cdl.countDown(); - } - }; - - Observable.range(1, 1).delay(1, TimeUnit.SECONDS).buffer(2, TimeUnit.SECONDS).unsafeSubscribe(s); - - cdl.await(); - - verify(o).onNext(Arrays.asList(1)); - verify(o).onCompleted(); - verify(o, never()).onError(any(Throwable.class)); - - assertFalse(s.isUnsubscribed()); + @SuppressWarnings("unchecked") + final Observer o = mock(Observer.class); + + + final CountDownLatch cdl = new CountDownLatch(1); + Subscriber s = new Subscriber() { + @Override + public void onNext(Object t) { + o.onNext(t); + } + @Override + public void onError(Throwable e) { + o.onError(e); + cdl.countDown(); + } + @Override + public void onCompleted() { + o.onCompleted(); + cdl.countDown(); + } + }; + + Observable.range(1, 1).delay(1, TimeUnit.SECONDS).buffer(2, TimeUnit.SECONDS).unsafeSubscribe(s); + + cdl.await(); + + verify(o).onNext(Arrays.asList(1)); + verify(o).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + + assertFalse(s.isUnsubscribed()); } } diff --git a/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java b/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java index aaaac6016e..f74d5d93f4 100644 --- a/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java @@ -153,11 +153,11 @@ public void testBackpressure() { Observable.range(0, 100000) .onErrorReturn(new Func1() { - @Override - public Integer call(Throwable t1) { - return 1; - } - + @Override + public Integer call(Throwable t1) { + return 1; + } + }) .observeOn(Schedulers.computation()) .map(new Func1() { From 0a0f6f7a900a39d8ec86faa783a9dd85c713458d Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 18 Jun 2015 11:35:35 -0700 Subject: [PATCH 309/857] Fix OperatorFlatMapPerf.flatMapIntPassthruAsync Perf Test This test was reported broken in https://github.com/ReactiveX/RxJava/pull/2928#issuecomment-113229698 Fixing by adding the use of LatchedObserver. Previously broken test results: ``` r.o.OperatorFlatMapPerf.flatMapIntPassthruAsync 1 thrpt 5 363615.622 115041.519 ops/s r.o.OperatorFlatMapPerf.flatMapIntPassthruAsync 1000 thrpt 5 350.204 125.773 ops/s r.o.OperatorFlatMapPerf.flatMapIntPassthruAsync 1000000 thrpt 5 0.319 0.184 ops/s ``` Fixed results: ``` r.o.OperatorFlatMapPerf.flatMapIntPassthruAsync 1 thrpt 5 102109.681 8709.920 ops/s r.o.OperatorFlatMapPerf.flatMapIntPassthruAsync 1000 thrpt 5 403.071 130.651 ops/s r.o.OperatorFlatMapPerf.flatMapIntPassthruAsync 1000000 thrpt 5 0.355 0.070 ops/s ``` --- src/perf/java/rx/operators/OperatorFlatMapPerf.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/perf/java/rx/operators/OperatorFlatMapPerf.java b/src/perf/java/rx/operators/OperatorFlatMapPerf.java index 6739fb0a1d..c20cff67f1 100644 --- a/src/perf/java/rx/operators/OperatorFlatMapPerf.java +++ b/src/perf/java/rx/operators/OperatorFlatMapPerf.java @@ -28,6 +28,7 @@ import rx.Observable; import rx.functions.Func1; import rx.jmh.InputWithIncrementingInteger; +import rx.jmh.LatchedObserver; import rx.schedulers.Schedulers; @BenchmarkMode(Mode.Throughput) @@ -62,6 +63,7 @@ public Observable call(Integer i) { @Benchmark public void flatMapIntPassthruAsync(Input input) throws InterruptedException { + LatchedObserver latchedObserver = input.newLatchedObserver(); input.observable.flatMap(new Func1>() { @Override @@ -69,7 +71,8 @@ public Observable call(Integer i) { return Observable.just(i).subscribeOn(Schedulers.computation()); } - }).subscribe(input.observer); + }).subscribe(latchedObserver); + latchedObserver.latch.await(); } @Benchmark From 2d5ce6935910cb3046384254329bd46236802796 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 19 Jun 2015 09:49:20 +1000 Subject: [PATCH 310/857] takeLast javadoc fixes, standardize on parameter names (count instead of num), improve message in OperatorTakeLast exception --- src/main/java/rx/Observable.java | 56 ++++++++++--------- .../internal/operators/OperatorTakeLast.java | 4 +- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c576ea59f9..8069a7b1da 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5141,28 +5141,28 @@ public final Observable lastOrDefault(T defaultValue, Func1 * Alias of {@link #take(int)} to match Java 8 Stream API naming convention. *

* *

* This method returns an Observable that will invoke a subscribing {@link Observer}'s - * {@link Observer#onNext onNext} function a maximum of {@code num} times before invoking + * {@link Observer#onNext onNext} function a maximum of {@code count} times before invoking * {@link Observer#onCompleted onCompleted}. *

*
Scheduler:
*
{@code limit} does not operate by default on a particular {@link Scheduler}.
*
* - * @param num + * @param count * the maximum number of items to emit - * @return an Observable that emits only the first {@code num} items emitted by the source Observable, or - * all of the items from the source Observable if that Observable emits fewer than {@code num} items + * @return an Observable that emits only the first {@code count} items emitted by the source Observable, or + * all of the items from the source Observable if that Observable emits fewer than {@code count} items * @see ReactiveX operators documentation: Take */ - public final Observable limit(int num) { - return take(num); + public final Observable limit(int count) { + return take(count); } /** @@ -6894,7 +6894,7 @@ public final Observable singleOrDefault(T defaultValue, Func1 * @@ -6903,14 +6903,14 @@ public final Observable singleOrDefault(T defaultValue, Func1This version of {@code skip} does not operate by default on a particular {@link Scheduler}. * * - * @param num + * @param count * the number of items to skip * @return an Observable that is identical to the source Observable except that it does not emit the first - * {@code num} items that the source Observable emits + * {@code count} items that the source Observable emits * @see ReactiveX operators documentation: Skip */ - public final Observable skip(int num) { - return lift(new OperatorSkip(num)); + public final Observable skip(int count) { + return lift(new OperatorSkip(count)); } /** @@ -7766,26 +7766,27 @@ public final Observable switchMap(Func1 * *

* This method returns an Observable that will invoke a subscribing {@link Observer}'s - * {@link Observer#onNext onNext} function a maximum of {@code num} times before invoking + * {@link Observer#onNext onNext} function a maximum of {@code count} times before invoking * {@link Observer#onCompleted onCompleted}. *

*
Scheduler:
*
This version of {@code take} does not operate by default on a particular {@link Scheduler}.
*
* - * @param num + * @param count * the maximum number of items to emit - * @return an Observable that emits only the first {@code num} items emitted by the source Observable, or - * all of the items from the source Observable if that Observable emits fewer than {@code num} items + * @return an Observable that emits only the first {@code count} items emitted by the source Observable, or + * all of the items from the source Observable if that Observable emits fewer than {@code count} items * @see ReactiveX operators documentation: Take */ - public final Observable take(final int num) { - return lift(new OperatorTake(num)); + public final Observable take(final int count) { + return lift(new OperatorTake(count)); } /** @@ -7855,7 +7856,8 @@ public final Observable takeFirst(Func1 predicate) { } /** - * Returns an Observable that emits only the last {@code count} items emitted by the source Observable. + * Returns an Observable that emits at most the last {@code count} items emitted by the source Observable. If the source emits fewer than + * {@code count} items then all of its items are emitted. *

* *

@@ -7864,9 +7866,9 @@ public final Observable takeFirst(Func1 predicate) { *
* * @param count - * the number of items to emit from the end of the sequence of items emitted by the source + * the maximum number of items to emit from the end of the sequence of items emitted by the source * Observable - * @return an Observable that emits only the last {@code count} items emitted by the source Observable + * @return an Observable that emits at most the last {@code count} items emitted by the source Observable * @throws IndexOutOfBoundsException * if {@code count} is less than zero * @see ReactiveX operators documentation: TakeLast @@ -7882,7 +7884,7 @@ else if (count == 1 ) /** * Returns an Observable that emits at most a specified number of items from the source Observable that were - * emitted in a specified window of time before the Observable completed. + * emitted in a specified window of time before the Observable completed. *

* *

@@ -7983,8 +7985,8 @@ public final Observable takeLast(long time, TimeUnit unit, Scheduler schedule } /** - * Returns an Observable that emits a single List containing the last {@code count} elements emitted by the - * source Observable. + * Returns an Observable that emits a single List containing at most the last {@code count} elements emitted by the + * source Observable. If the source emits fewer than {@code count} items then the emitted List will contain all of the source emissions. *

* *

@@ -7993,8 +7995,8 @@ public final Observable takeLast(long time, TimeUnit unit, Scheduler schedule *
* * @param count - * the number of items to emit in the list - * @return an Observable that emits a single list containing the last {@code count} elements emitted by the + * the maximum number of items to emit in the list + * @return an Observable that emits a single list containing at most the last {@code count} elements emitted by the * source Observable * @see ReactiveX operators documentation: TakeLast */ diff --git a/src/main/java/rx/internal/operators/OperatorTakeLast.java b/src/main/java/rx/internal/operators/OperatorTakeLast.java index fb547f711b..54c1a0c43a 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLast.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLast.java @@ -22,7 +22,7 @@ import rx.Subscriber; /** - * Returns an Observable that emits the last count items emitted by the source Observable. + * Returns an Observable that emits the at most the last count items emitted by the source Observable. *

* */ @@ -32,7 +32,7 @@ public final class OperatorTakeLast implements Operator { public OperatorTakeLast(int count) { if (count < 0) { - throw new IndexOutOfBoundsException("count could not be negative"); + throw new IndexOutOfBoundsException("count cannot be negative"); } this.count = count; } From 935822cee960b54a2c0bcbae26b09d06db8ab895 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 19 Jun 2015 10:25:24 +1000 Subject: [PATCH 311/857] instantiate EMPTY lazily --- src/main/java/rx/Observable.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c576ea59f9..ed665b0345 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1029,13 +1029,15 @@ public final static Observable defer(Func0> observableFacto return create(new OnSubscribeDefer(observableFactory)); } - /** An empty observable which just emits onCompleted to any subscriber. */ - private static final Observable EMPTY = create(new OnSubscribe() { - @Override - public void call(Subscriber t1) { - t1.onCompleted(); - } - }); + /** 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 @@ -1055,7 +1057,7 @@ public void call(Subscriber t1) { */ @SuppressWarnings("unchecked") public final static Observable empty() { - return (Observable)EMPTY; + return (Observable) EmptyHolder.INSTANCE; } /** From 39d3bd97b2472de77727b00768abc2767aa30624 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 22 Jun 2015 10:18:02 +0200 Subject: [PATCH 312/857] Window with boundary observable: fixed unsubscription and behavior. --- src/main/java/rx/Observable.java | 2 +- .../OperatorWindowWithObservable.java | 38 +-- .../OperatorWindowWithObservableFactory.java | 320 ++++++++++++++++++ .../OperatorWindowWithStartEndObservable.java | 83 +++-- .../OperatorWindowWithObservableTest.java | 174 +++++++++- ...ratorWindowWithStartEndObservableTest.java | 96 +++++- 6 files changed, 624 insertions(+), 89 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index ed665b0345..2ba02dd110 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -9060,7 +9060,7 @@ public final Observable withLatestFrom(Observable other, * @see ReactiveX operators documentation: Window */ public final Observable> window(Func0> closingSelector) { - return lift(new OperatorWindowWithObservable(closingSelector)); + return lift(new OperatorWindowWithObservableFactory(closingSelector)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java b/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java index c5fec0a13d..3b7e1c1cac 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java @@ -15,16 +15,13 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import rx.Observable; +import java.util.*; + +import rx.*; import rx.Observable.Operator; +import rx.Observable; import rx.Observer; -import rx.Subscriber; -import rx.functions.Func0; import rx.observers.SerializedSubscriber; -import rx.observers.Subscribers; /** * Creates non-overlapping windows of items where each window is terminated by @@ -34,36 +31,21 @@ * @param the boundary value type */ public final class OperatorWindowWithObservable implements Operator, T> { - final Func0> otherFactory; + final Observable other; - public OperatorWindowWithObservable(Func0> otherFactory) { - this.otherFactory = otherFactory; - } public OperatorWindowWithObservable(final Observable other) { - this.otherFactory = new Func0>() { - - @Override - public Observable call() { - return other; - } - - }; + this.other = other; } @Override public Subscriber call(Subscriber> child) { - Observable other; - try { - other = otherFactory.call(); - } catch (Throwable e) { - child.onError(e); - return Subscribers.empty(); - } - SourceSubscriber sub = new SourceSubscriber(child); BoundarySubscriber bs = new BoundarySubscriber(child, sub); + child.add(sub); + child.add(bs); + sub.replaceWindow(); other.unsafeSubscribe(bs); @@ -88,7 +70,6 @@ static final class SourceSubscriber extends Subscriber { List queue; public SourceSubscriber(Subscriber> child) { - super(child); this.child = new SerializedSubscriber>(child); this.guard = new Object(); } @@ -288,7 +269,6 @@ void error(Throwable e) { static final class BoundarySubscriber extends Subscriber { final SourceSubscriber sub; public BoundarySubscriber(Subscriber child, SourceSubscriber sub) { - super(child); this.sub = sub; } diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java b/src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java new file mode 100644 index 0000000000..a764850c79 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java @@ -0,0 +1,320 @@ +/** + * 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.*; + +import rx.*; +import rx.Observable.Operator; +import rx.Observable; +import rx.Observer; +import rx.functions.Func0; +import rx.observers.SerializedSubscriber; +import rx.subscriptions.SerialSubscription; + +/** + * Creates non-overlapping windows of items where each window is terminated by + * an event from a secondary observable and a new window is started immediately. + * + * @param the value type + * @param the boundary value type + */ +public final class OperatorWindowWithObservableFactory implements Operator, T> { + final Func0> otherFactory; + + public OperatorWindowWithObservableFactory(Func0> otherFactory) { + this.otherFactory = otherFactory; + } + + @Override + public Subscriber call(Subscriber> child) { + + SourceSubscriber sub = new SourceSubscriber(child, otherFactory); + + child.add(sub); + + sub.replaceWindow(); + + return sub; + } + /** Indicate the current subject should complete and a new subject be emitted. */ + static final Object NEXT_SUBJECT = new Object(); + /** For error and completion indication. */ + static final NotificationLite nl = NotificationLite.instance(); + /** Observes the source. */ + static final class SourceSubscriber extends Subscriber { + final Subscriber> child; + final Object guard; + /** Accessed from the serialized part. */ + Observer consumer; + /** Accessed from the serialized part. */ + Observable producer; + /** Guarded by guard. */ + boolean emitting; + /** Guarded by guard. */ + List queue; + + final SerialSubscription ssub; + + final Func0> otherFactory; + + public SourceSubscriber(Subscriber> child, + Func0> otherFactory) { + this.child = new SerializedSubscriber>(child); + this.guard = new Object(); + this.ssub = new SerialSubscription(); + this.otherFactory = otherFactory; + this.add(ssub); + } + + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + List localQueue; + synchronized (guard) { + if (emitting) { + if (queue == null) { + queue = new ArrayList(); + } + queue.add(t); + return; + } + localQueue = queue; + queue = null; + emitting = true; + } + boolean once = true; + boolean skipFinal = false; + try { + do { + drain(localQueue); + if (once) { + once = false; + emitValue(t); + } + + synchronized (guard) { + localQueue = queue; + queue = null; + if (localQueue == null) { + emitting = false; + skipFinal = true; + return; + } + } + } while (!child.isUnsubscribed()); + } finally { + if (!skipFinal) { + synchronized (guard) { + emitting = false; + } + } + } + } + + void drain(List queue) { + if (queue == null) { + return; + } + for (Object o : queue) { + if (o == NEXT_SUBJECT) { + replaceSubject(); + } else + if (nl.isError(o)) { + error(nl.getError(o)); + break; + } else + if (nl.isCompleted(o)) { + complete(); + break; + } else { + @SuppressWarnings("unchecked") + T t = (T)o; + emitValue(t); + } + } + } + void replaceSubject() { + Observer s = consumer; + if (s != null) { + s.onCompleted(); + } + createNewWindow(); + child.onNext(producer); + } + void createNewWindow() { + BufferUntilSubscriber bus = BufferUntilSubscriber.create(); + consumer = bus; + producer = bus; + Observable other; + try { + other = otherFactory.call(); + } catch (Throwable e) { + child.onError(e); + unsubscribe(); + return; + } + + BoundarySubscriber bs = new BoundarySubscriber(child, this); + ssub.set(bs); + other.unsafeSubscribe(bs); + } + void emitValue(T t) { + Observer s = consumer; + if (s != null) { + s.onNext(t); + } + } + + @Override + public void onError(Throwable e) { + synchronized (guard) { + if (emitting) { + queue = Collections.singletonList(nl.error(e)); + return; + } + queue = null; + emitting = true; + } + error(e); + } + + @Override + public void onCompleted() { + List localQueue; + synchronized (guard) { + if (emitting) { + if (queue == null) { + queue = new ArrayList(); + } + queue.add(nl.completed()); + return; + } + localQueue = queue; + queue = null; + emitting = true; + } + try { + drain(localQueue); + } catch (Throwable e) { + error(e); + return; + } + complete(); + } + void replaceWindow() { + List localQueue; + synchronized (guard) { + if (emitting) { + if (queue == null) { + queue = new ArrayList(); + } + queue.add(NEXT_SUBJECT); + return; + } + localQueue = queue; + queue = null; + emitting = true; + } + boolean once = true; + boolean skipFinal = false; + try { + do { + drain(localQueue); + if (once) { + once = false; + replaceSubject(); + } + synchronized (guard) { + localQueue = queue; + queue = null; + if (localQueue == null) { + emitting = false; + skipFinal = true; + return; + } + } + } while (!child.isUnsubscribed()); + } finally { + if (!skipFinal) { + synchronized (guard) { + emitting = false; + } + } + } + } + void complete() { + Observer s = consumer; + consumer = null; + producer = null; + + if (s != null) { + s.onCompleted(); + } + child.onCompleted(); + unsubscribe(); + } + void error(Throwable e) { + Observer s = consumer; + consumer = null; + producer = null; + + if (s != null) { + s.onError(e); + } + child.onError(e); + unsubscribe(); + } + } + /** Observes the boundary. */ + static final class BoundarySubscriber extends Subscriber { + final SourceSubscriber sub; + boolean done; + public BoundarySubscriber(Subscriber child, SourceSubscriber sub) { + this.sub = sub; + } + + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onNext(U t) { + if (!done) { + done = true; + sub.replaceWindow(); + } + } + + @Override + public void onError(Throwable e) { + sub.onError(e); + } + + @Override + public void onCompleted() { + if (!done) { + done = true; + sub.onCompleted(); + } + } + } +} diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java b/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java index e884cd01d7..82d1474163 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java @@ -15,17 +15,14 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import rx.Observable; +import java.util.*; + +import rx.*; import rx.Observable.Operator; +import rx.Observable; import rx.Observer; -import rx.Subscriber; import rx.functions.Func1; -import rx.observers.SerializedObserver; -import rx.observers.SerializedSubscriber; +import rx.observers.*; import rx.subscriptions.CompositeSubscription; /** @@ -49,9 +46,12 @@ public OperatorWindowWithStartEndObservable(Observable windowOpenin @Override public Subscriber call(Subscriber> child) { - final SourceSubscriber sub = new SourceSubscriber(child); + CompositeSubscription csub = new CompositeSubscription(); + child.add(csub); + + final SourceSubscriber sub = new SourceSubscriber(child, csub); - Subscriber open = new Subscriber(child) { + Subscriber open = new Subscriber() { @Override public void onStart() { @@ -73,7 +73,10 @@ public void onCompleted() { sub.onCompleted(); } }; - + + csub.add(sub); + csub.add(open); + windowOpenings.unsafeSubscribe(open); return sub; @@ -97,13 +100,11 @@ final class SourceSubscriber extends Subscriber { final List> chunks; /** Guarded by guard. */ boolean done; - public SourceSubscriber(Subscriber> child) { - super(child); + public SourceSubscriber(Subscriber> child, CompositeSubscription csub) { this.child = new SerializedSubscriber>(child); this.guard = new Object(); this.chunks = new LinkedList>(); - this.csub = new CompositeSubscription(); - child.add(csub); + this.csub = csub; } @Override @@ -127,36 +128,44 @@ public void onNext(T t) { @Override public void onError(Throwable e) { - List> list; - synchronized (guard) { - if (done) { - return; + try { + List> list; + synchronized (guard) { + if (done) { + return; + } + done = true; + list = new ArrayList>(chunks); + chunks.clear(); } - done = true; - list = new ArrayList>(chunks); - chunks.clear(); - } - for (SerializedSubject cs : list) { - cs.consumer.onError(e); + for (SerializedSubject cs : list) { + cs.consumer.onError(e); + } + child.onError(e); + } finally { + csub.unsubscribe(); } - child.onError(e); } @Override public void onCompleted() { - List> list; - synchronized (guard) { - if (done) { - return; + try { + List> list; + synchronized (guard) { + if (done) { + return; + } + done = true; + list = new ArrayList>(chunks); + chunks.clear(); } - done = true; - list = new ArrayList>(chunks); - chunks.clear(); - } - for (SerializedSubject cs : list) { - cs.consumer.onCompleted(); + for (SerializedSubject cs : list) { + cs.consumer.onCompleted(); + } + child.onCompleted(); + } finally { + csub.unsubscribe(); } - child.onCompleted(); } void beginWindow(U token) { diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java index fd8a20fed3..05488379c2 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java @@ -15,15 +15,12 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; @@ -290,4 +287,167 @@ public Observable call() { assertEquals(1, ts.getOnNextEvents().size()); assertEquals(Arrays.asList(1, 2), tsw.getOnNextEvents()); } + + @Test + public void testWindowViaObservableNoUnsubscribe() { + Observable source = Observable.range(1, 10); + Func0> boundary = new Func0>() { + @Override + public Observable call() { + return Observable.empty(); + } + }; + + TestSubscriber> ts = TestSubscriber.create(); + source.window(boundary).unsafeSubscribe(ts); + + assertFalse(ts.isUnsubscribed()); + } + + @Test + public void testBoundaryUnsubscribedOnMainCompletion() { + PublishSubject source = PublishSubject.create(); + final PublishSubject boundary = PublishSubject.create(); + Func0> boundaryFunc = new Func0>() { + @Override + public Observable call() { + return boundary; + } + }; + + TestSubscriber> ts = TestSubscriber.create(); + source.window(boundaryFunc).subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(boundary.hasObservers()); + + source.onCompleted(); + + assertFalse(source.hasObservers()); + assertFalse(boundary.hasObservers()); + + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertValueCount(1); + } + @Test + public void testMainUnsubscribedOnBoundaryCompletion() { + PublishSubject source = PublishSubject.create(); + final PublishSubject boundary = PublishSubject.create(); + Func0> boundaryFunc = new Func0>() { + @Override + public Observable call() { + return boundary; + } + }; + + TestSubscriber> ts = TestSubscriber.create(); + source.window(boundaryFunc).subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(boundary.hasObservers()); + + boundary.onCompleted(); + + assertFalse(source.hasObservers()); + assertFalse(boundary.hasObservers()); + + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertValueCount(1); + } + @Test + public void testChildUnsubscribed() { + PublishSubject source = PublishSubject.create(); + final PublishSubject boundary = PublishSubject.create(); + Func0> boundaryFunc = new Func0>() { + @Override + public Observable call() { + return boundary; + } + }; + + TestSubscriber> ts = TestSubscriber.create(); + source.window(boundaryFunc).subscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(boundary.hasObservers()); + + ts.unsubscribe(); + + assertFalse(source.hasObservers()); + assertFalse(boundary.hasObservers()); + + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertValueCount(1); + } + @Test + public void testNoBackpressure() { + Observable source = Observable.range(1, 10); + final PublishSubject boundary = PublishSubject.create(); + Func0> boundaryFunc = new Func0>() { + @Override + public Observable call() { + return boundary; + } + }; + + final TestSubscriber ts = TestSubscriber.create(1); + final TestSubscriber> ts1 = new TestSubscriber>(1) { + @Override + public void onNext(Observable t) { + super.onNext(t); + t.subscribe(ts); + } + }; + source.window(boundaryFunc) + .subscribe(ts1); + + ts1.assertNoErrors(); + ts1.assertCompleted(); + ts1.assertValueCount(1); + + ts.assertNoErrors(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts.assertCompleted(); + } + @Test + public void newBoundaryCalledAfterWindowClosed() { + final AtomicInteger calls = new AtomicInteger(); + PublishSubject source = PublishSubject.create(); + final PublishSubject boundary = PublishSubject.create(); + Func0> boundaryFunc = new Func0>() { + @Override + public Observable call() { + calls.getAndIncrement(); + return boundary; + } + }; + + TestSubscriber> ts = TestSubscriber.create(); + source.window(boundaryFunc).subscribe(ts); + + source.onNext(1); + boundary.onNext(1); + assertTrue(boundary.hasObservers()); + + source.onNext(2); + boundary.onNext(2); + assertTrue(boundary.hasObservers()); + + source.onNext(3); + boundary.onNext(3); + assertTrue(boundary.hasObservers()); + + source.onNext(4); + source.onCompleted(); + + ts.assertNoErrors(); + ts.assertValueCount(4); + ts.assertCompleted(); + + assertFalse(source.hasObservers()); + assertFalse(boundary.hasObservers()); + } } \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithStartEndObservableTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithStartEndObservableTest.java index 492bf3cb49..be3c16e660 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithStartEndObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithStartEndObservableTest.java @@ -15,25 +15,20 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; +import rx.*; import rx.Observable; import rx.Observer; -import rx.Scheduler; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func0; -import rx.functions.Func1; +import rx.functions.*; +import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; +import rx.subjects.PublishSubject; public class OperatorWindowWithStartEndObservableTest { @@ -112,14 +107,21 @@ public void call(Subscriber observer) { }); Func0> closer = new Func0>() { + int calls; @Override public Observable call() { return Observable.create(new Observable.OnSubscribe() { @Override public void call(Subscriber observer) { - push(observer, new Object(), 100); - push(observer, new Object(), 200); - complete(observer, 301); + int c = calls++; + if (c == 0) { + push(observer, new Object(), 100); + } else + if (c == 1) { + push(observer, new Object(), 100); + } else { + complete(observer, 101); + } } }); } @@ -185,4 +187,68 @@ public void onNext(String args) { } }; } + + @Test + public void testNoUnsubscribeAndNoLeak() { + PublishSubject source = PublishSubject.create(); + + PublishSubject open = PublishSubject.create(); + final PublishSubject close = PublishSubject.create(); + + TestSubscriber> ts = TestSubscriber.create(); + + source.window(open, new Func1>() { + @Override + public Observable call(Integer t) { + return close; + } + }).unsafeSubscribe(ts); + + open.onNext(1); + source.onNext(1); + + assertTrue(open.hasObservers()); + assertTrue(close.hasObservers()); + + close.onNext(1); + + assertFalse(close.hasObservers()); + + source.onCompleted(); + + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertValueCount(1); + + assertFalse(ts.isUnsubscribed()); + assertFalse(open.hasObservers()); + assertFalse(close.hasObservers()); + } + + @Test + public void testUnsubscribeAll() { + PublishSubject source = PublishSubject.create(); + + PublishSubject open = PublishSubject.create(); + final PublishSubject close = PublishSubject.create(); + + TestSubscriber> ts = TestSubscriber.create(); + + source.window(open, new Func1>() { + @Override + public Observable call(Integer t) { + return close; + } + }).unsafeSubscribe(ts); + + open.onNext(1); + + assertTrue(open.hasObservers()); + assertTrue(close.hasObservers()); + + ts.unsubscribe(); + + assertFalse(open.hasObservers()); + assertFalse(close.hasObservers()); + } } \ No newline at end of file From acb1259c35377058220c7a664011a8d4c30cd510 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 23 Jun 2015 16:35:32 +1000 Subject: [PATCH 313/857] TestSubscriber javadoc improvements --- .../java/rx/observers/TestSubscriber.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index cb4d607bed..a2255cf401 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -143,7 +143,7 @@ public void onCompleted() { } /** - * Get the {@link Notification}s representing each time this {@link Subscriber} was notified of sequence + * Returns the {@link Notification}s representing each time this {@link Subscriber} was notified of sequence * completion via {@link #onCompleted}, as a {@link List}. * * @return a list of Notifications representing calls to this Subscriber's {@link #onCompleted} method @@ -172,7 +172,7 @@ public void onError(Throwable e) { } /** - * Get the {@link Throwable}s this {@link Subscriber} was notified of via {@link #onError} as a + * Returns the {@link Throwable}s this {@link Subscriber} was notified of via {@link #onError} as a * {@link List}. * * @return a list of the Throwables that were passed to this Subscriber's {@link #onError} method @@ -199,7 +199,7 @@ public void onNext(T t) { } /** - * Allow calling the protected {@link #request(long)} from unit tests. + * Allows calling the protected {@link #request(long)} from unit tests. * * @param n the maximum number of items you want the Observable to emit to the Subscriber at this time, or * {@code Long.MAX_VALUE} if you want the Observable to emit items at its own pace @@ -209,7 +209,7 @@ public void requestMore(long n) { } /** - * Get the sequence of items observed by this {@link Subscriber}, as an ordered {@link List}. + * Returns the sequence of items observed by this {@link Subscriber}, as an ordered {@link List}. * * @return a list of items observed by this Subscriber, in the order in which they were observed */ @@ -218,7 +218,7 @@ public List getOnNextEvents() { } /** - * Assert that a particular sequence of items was received by this {@link Subscriber} in order. + * Asserts that a particular sequence of items was received by this {@link Subscriber} in order. * * @param items * the sequence of items expected to have been observed @@ -230,7 +230,7 @@ public void assertReceivedOnNext(List items) { } /** - * Assert that a single terminal event occurred, either {@link #onCompleted} or {@link #onError}. + * Asserts that a single terminal event occurred, either {@link #onCompleted} or {@link #onError}. * * @throws AssertionError * if not exactly one terminal event notification was received @@ -240,7 +240,7 @@ public void assertTerminalEvent() { } /** - * Assert that this {@code Subscriber} is unsubscribed. + * Asserts that this {@code Subscriber} is unsubscribed. * * @throws AssertionError * if this {@code Subscriber} is not unsubscribed @@ -252,7 +252,7 @@ public void assertUnsubscribed() { } /** - * Assert that this {@code Subscriber} has received no {@code onError} notifications. + * Asserts that this {@code Subscriber} has received no {@code onError} notifications. * * @throws AssertionError * if this {@code Subscriber} has received one or more {@code onError} notifications @@ -335,7 +335,7 @@ public Thread getLastSeenThread() { } /** - * Assert if there is exactly a single completion event. + * Asserts that there is exactly one completion event. * * @throws AssertionError if there were zero, or more than one, onCompleted events * @since (if this graduates from "Experimental" replace this parenthetical with the release number) @@ -352,7 +352,7 @@ public void assertCompleted() { } /** - * Assert if there is no completion event. + * Asserts that there is no completion event. * * @throws AssertionError if there were one or more than one onCompleted events * @since (if this graduates from "Experimental" replace this parenthetical with the release number) @@ -369,7 +369,7 @@ public void assertNotCompleted() { } /** - * Assert if there is exactly one error event which is a subclass of the given class. + * Asserts that there is exactly one error event which is a subclass of the given class. * * @param clazz the class to check the error against. * @throws AssertionError if there were zero, or more than one, onError events, or if the single onError @@ -395,7 +395,7 @@ public void assertError(Class clazz) { } /** - * Assert there is a single onError event with the exact exception. + * Asserts that there is a single onError event with the exact exception. * * @param throwable the throwable to check * @throws AssertionError if there were zero, or more than one, onError events, or if the single onError @@ -421,7 +421,7 @@ public void assertError(Throwable throwable) { } /** - * Assert for no onError and onCompleted events. + * Asserts that there are no onError and onCompleted events. * * @throws AssertionError if there was either an onError or onCompleted event * @since (if this graduates from "Experimental" replace this parenthetical with the release number) @@ -447,7 +447,7 @@ public void assertNoTerminalEvent() { } /** - * Assert if there are no onNext events received. + * Asserts that there are no onNext events received. * * @throws AssertionError if there were any onNext events * @since (if this graduates from "Experimental" replace this parenthetical with the release number) @@ -461,7 +461,7 @@ public void assertNoValues() { } /** - * Assert if the given number of onNext events are received. + * Asserts that the given number of onNext events are received. * * @param count the expected number of onNext events * @throws AssertionError if there were more or fewer onNext events than specified by {@code count} @@ -476,7 +476,7 @@ public void assertValueCount(int count) { } /** - * Assert if the received onNext events, in order, are the specified items. + * Asserts that the received onNext events, in order, are the specified items. * * @param values the items to check * @throws AssertionError if the items emitted do not exactly match those specified by {@code values} @@ -488,7 +488,7 @@ public void assertValues(T... values) { } /** - * Assert if there is only a single received onNext event and that it marks the emission of a specific item. + * Asserts that there is only a single received onNext event and that it marks the emission of a specific item. * * @param value the item to check * @throws AssertionError if the Observable does not emit only the single item specified by {@code value} From 60924afdc5c05f51470141877a455e887a7b3415 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 23 Jun 2015 16:19:23 +1000 Subject: [PATCH 314/857] add backpressure support for defaultIfEmpty() --- src/main/java/rx/Observable.java | 11 +++- .../operators/OperatorDefaultIfEmpty.java | 64 ------------------- .../operators/OperatorDefaultIfEmptyTest.java | 25 ++++++++ 3 files changed, 34 insertions(+), 66 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OperatorDefaultIfEmpty.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 421a5ea89d..776026fdb4 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -19,6 +19,7 @@ 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; @@ -3857,8 +3858,14 @@ public final Observable debounce(long timeout, TimeUnit unit, Scheduler sched * items, or the items emitted by the source Observable * @see ReactiveX operators documentation: DefaultIfEmpty */ - public final Observable defaultIfEmpty(T defaultValue) { - return lift(new OperatorDefaultIfEmpty(defaultValue)); + 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)); + }})); } /** diff --git a/src/main/java/rx/internal/operators/OperatorDefaultIfEmpty.java b/src/main/java/rx/internal/operators/OperatorDefaultIfEmpty.java deleted file mode 100644 index 1265f81907..0000000000 --- a/src/main/java/rx/internal/operators/OperatorDefaultIfEmpty.java +++ /dev/null @@ -1,64 +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 rx.Observable.Operator; -import rx.Subscriber; - -/** - * Returns the elements of the specified sequence or the specified default value - * in a singleton sequence if the sequence is empty. - * @param the value type - */ -public class OperatorDefaultIfEmpty implements Operator { - final T defaultValue; - - public OperatorDefaultIfEmpty(T defaultValue) { - this.defaultValue = defaultValue; - } - - @Override - public Subscriber call(final Subscriber child) { - return new Subscriber(child) { - boolean hasValue; - @Override - public void onNext(T t) { - hasValue = true; - child.onNext(t); - } - - @Override - public void onError(Throwable e) { - child.onError(e); - } - - @Override - public void onCompleted() { - if (!hasValue) { - try { - child.onNext(defaultValue); - } catch (Throwable e) { - child.onError(e); - return; - } - } - child.onCompleted(); - } - - }; - } - -} diff --git a/src/test/java/rx/internal/operators/OperatorDefaultIfEmptyTest.java b/src/test/java/rx/internal/operators/OperatorDefaultIfEmptyTest.java index a180f933e2..ec6bb0486f 100644 --- a/src/test/java/rx/internal/operators/OperatorDefaultIfEmptyTest.java +++ b/src/test/java/rx/internal/operators/OperatorDefaultIfEmptyTest.java @@ -26,6 +26,7 @@ import rx.Observer; import rx.Subscriber; import rx.exceptions.TestException; +import rx.observers.TestSubscriber; public class OperatorDefaultIfEmptyTest { @@ -85,4 +86,28 @@ public void onCompleted() { verify(o, never()).onNext(any(Integer.class)); verify(o, never()).onCompleted(); } + + @Test + public void testBackpressureEmpty() { + TestSubscriber ts = TestSubscriber.create(0); + Observable.empty().defaultIfEmpty(1).subscribe(ts); + ts.assertNoValues(); + ts.assertNoTerminalEvent(); + ts.requestMore(1); + ts.assertValue(1); + ts.assertCompleted(); + } + + @Test + public void testBackpressureNonEmpty() { + TestSubscriber ts = TestSubscriber.create(0); + Observable.just(1,2,3).defaultIfEmpty(1).subscribe(ts); + ts.assertNoValues(); + ts.assertNoTerminalEvent(); + ts.requestMore(2); + ts.assertValues(1, 2); + ts.requestMore(1); + ts.assertValues(1, 2, 3); + ts.assertCompleted(); + } } From 90ef143b09c992235d750a2d4048850f9f7a709b Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 23 Jun 2015 09:22:47 +0200 Subject: [PATCH 315/857] ElementAt request management enhanced --- .../internal/operators/OperatorElementAt.java | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorElementAt.java b/src/main/java/rx/internal/operators/OperatorElementAt.java index 844eb8bbad..19a156dfa2 100644 --- a/src/main/java/rx/internal/operators/OperatorElementAt.java +++ b/src/main/java/rx/internal/operators/OperatorElementAt.java @@ -15,7 +15,10 @@ */ package rx.internal.operators; +import java.util.concurrent.atomic.AtomicBoolean; + import rx.Observable.Operator; +import rx.Producer; import rx.Subscriber; /** @@ -45,25 +48,23 @@ private OperatorElementAt(int index, T defaultValue, boolean hasDefault) { } @Override - public Subscriber call(final Subscriber subscriber) { - return new Subscriber(subscriber) { + public Subscriber call(final Subscriber child) { + Subscriber parent = new Subscriber() { private int currentIndex = 0; @Override public void onNext(T value) { - if (currentIndex == index) { - subscriber.onNext(value); - subscriber.onCompleted(); - } else { - request(1); + if (currentIndex++ == index) { + child.onNext(value); + child.onCompleted(); + unsubscribe(); } - currentIndex++; } @Override public void onError(Throwable e) { - subscriber.onError(e); + child.onError(e); } @Override @@ -71,14 +72,46 @@ public void onCompleted() { if (currentIndex <= index) { // If "subscriber.onNext(value)" is called, currentIndex must be greater than index if (hasDefault) { - subscriber.onNext(defaultValue); - subscriber.onCompleted(); + child.onNext(defaultValue); + child.onCompleted(); } else { - subscriber.onError(new IndexOutOfBoundsException(index + " is out of bounds")); + child.onError(new IndexOutOfBoundsException(index + " is out of bounds")); } } } + + @Override + public void setProducer(Producer p) { + child.setProducer(new InnerProducer(p)); + } }; + child.add(parent); + + return parent; + } + /** + * A producer that wraps another Producer and requests Long.MAX_VALUE + * when the first positive request() call comes in. + */ + static class InnerProducer extends AtomicBoolean implements Producer { + /** */ + private static final long serialVersionUID = 1L; + + final Producer actual; + + public InnerProducer(Producer actual) { + this.actual = actual; + } + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + if (n > 0 && compareAndSet(false, true)) { + // trigger the fast-path since the operator is going + // to skip all but the indexth element + actual.request(Long.MAX_VALUE); + } + } } - } From 82d7b9cca2efd0a8f36ec3b700bb8f34c445a093 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 23 Jun 2015 17:31:14 +0200 Subject: [PATCH 316/857] Operator replay() now supports backpressure --- src/main/java/rx/Observable.java | 213 ++- .../rx/internal/operators/OperatorReplay.java | 1176 ++++++++++++++++- .../operators/OperatorReplayTest.java | 160 ++- 3 files changed, 1354 insertions(+), 195 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 776026fdb4..c328582800 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -25,7 +25,6 @@ import rx.observers.SafeSubscriber; import rx.plugins.*; import rx.schedulers.*; -import rx.subjects.*; import rx.subscriptions.Subscriptions; /** @@ -5883,9 +5882,9 @@ public Void call(Notification notification) { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -5895,14 +5894,7 @@ public Void call(Notification notification) { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay() { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject. create(); - } - - }); + return OperatorReplay.create(this); } /** @@ -5912,9 +5904,9 @@ public final ConnectableObservable replay() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -5929,12 +5921,12 @@ public final ConnectableObservable replay() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.create(); + public ConnectableObservable call() { + return Observable.this.replay(); } - }, selector)); + }, selector); } /** @@ -5945,9 +5937,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -5965,12 +5957,12 @@ public final Subject call() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.createWithSize(bufferSize); + public ConnectableObservable call() { + return Observable.this.replay(bufferSize); } - }, selector)); + }, selector); } /** @@ -5981,9 +5973,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6017,9 +6009,9 @@ public final Observable replay(Func1, ? extends Obs * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6049,12 +6041,12 @@ public final Observable replay(Func1, ? extends Obs if (bufferSize < 0) { throw new IllegalArgumentException("bufferSize < 0"); } - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.createWithTimeAndSize(time, unit, bufferSize, scheduler); + public ConnectableObservable call() { + return Observable.this.replay(bufferSize, time, unit, scheduler); } - }, selector)); + }, selector); } /** @@ -6065,9 +6057,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6086,13 +6078,18 @@ public final Subject call() { * replaying no more than {@code bufferSize} notifications * @see ReactiveX operators documentation: Replay */ - public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize, final Scheduler scheduler) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + public final Observable replay(final Func1, ? extends Observable> selector, final int bufferSize, final Scheduler scheduler) { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return OperatorReplay. createScheduledSubject(ReplaySubject.createWithSize(bufferSize), scheduler); + public ConnectableObservable call() { + return Observable.this.replay(bufferSize); } - }, selector)); + }, new Func1, Observable>() { + @Override + public Observable call(Observable t) { + return selector.call(t).observeOn(scheduler); + } + }); } /** @@ -6103,9 +6100,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6136,9 +6133,9 @@ public final Observable replay(Func1, ? extends Obs * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6160,12 +6157,12 @@ 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 create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.createWithTime(time, unit, scheduler); + public ConnectableObservable call() { + return Observable.this.replay(time, unit, scheduler); } - }, selector)); + }, selector); } /** @@ -6175,9 +6172,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6194,13 +6191,18 @@ public final Subject call() { * replaying all items * @see ReactiveX operators documentation: Replay */ - public final Observable replay(Func1, ? extends Observable> selector, final Scheduler scheduler) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + public final Observable replay(final Func1, ? extends Observable> selector, final Scheduler scheduler) { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return OperatorReplay.createScheduledSubject(ReplaySubject. create(), scheduler); + public ConnectableObservable call() { + return Observable.this.replay(); } - }, selector)); + }, new Func1, Observable>() { + @Override + public Observable call(Observable t) { + return selector.call(t).observeOn(scheduler); + } + }); } /** @@ -6212,9 +6214,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -6226,14 +6228,7 @@ public final Subject call() { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject.createWithSize(bufferSize); - } - - }); + return OperatorReplay.create(this, bufferSize); } /** @@ -6245,9 +6240,9 @@ public final ConnectableObservable replay(final int bufferSize) { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6276,9 +6271,9 @@ public final ConnectableObservable replay(int bufferSize, long time, TimeUnit * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6302,14 +6297,7 @@ public final ConnectableObservable replay(final int bufferSize, final long ti if (bufferSize < 0) { throw new IllegalArgumentException("bufferSize < 0"); } - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject.createWithTimeAndSize(time, unit, bufferSize, scheduler); - } - - }); + return OperatorReplay.create(this, time, unit, scheduler, bufferSize); } /** @@ -6321,9 +6309,9 @@ public final ConnectableObservable replay(final int bufferSize, final long ti * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6337,14 +6325,7 @@ public final ConnectableObservable replay(final int bufferSize, final long ti * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize, final Scheduler scheduler) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return OperatorReplay.createScheduledSubject(ReplaySubject.createWithSize(bufferSize), scheduler); - } - - }); + return OperatorReplay.observeOn(replay(bufferSize), scheduler); } /** @@ -6356,9 +6337,9 @@ public final ConnectableObservable replay(final int bufferSize, final Schedul * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6384,9 +6365,9 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6402,14 +6383,7 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final long time, final TimeUnit unit, final Scheduler scheduler) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject.createWithTime(time, unit, scheduler); - } - - }); + return OperatorReplay.create(this, time, unit, scheduler); } /** @@ -6421,9 +6395,9 @@ public final ConnectableObservable replay(final long time, final TimeUnit uni * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6436,14 +6410,7 @@ public final ConnectableObservable replay(final long time, final TimeUnit uni * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final Scheduler scheduler) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return OperatorReplay.createScheduledSubject(ReplaySubject. create(), scheduler); - } - - }); + return OperatorReplay.observeOn(replay(), scheduler); } /** diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index 83c76dfe39..77f19edf32 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -15,93 +15,1163 @@ */ package rx.internal.operators; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; +import rx.*; import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Scheduler; -import rx.Subscriber; -import rx.subjects.Subject; +import rx.exceptions.Exceptions; +import rx.functions.*; +import rx.observables.ConnectableObservable; +import rx.schedulers.Timestamped; +import rx.subscriptions.Subscriptions; -/** - * Replay with limited buffer and/or time constraints. - * - * - * @see MSDN: Observable.Replay overloads - */ -public final class OperatorReplay { - /** Utility class. */ - private OperatorReplay() { - throw new IllegalStateException("No instances!"); +public final class OperatorReplay extends ConnectableObservable { + /** The source observable. */ + final Observable source; + /** Holds the current subscriber that is, will be or just was subscribed to the source observable. */ + final AtomicReference> current; + /** A factory that creates the appropriate buffer for the ReplaySubscriber. */ + final Func0> bufferFactory; + + @SuppressWarnings("rawtypes") + static final Func0 DEFAULT_UNBOUNDED_FACTORY = new Func0() { + @Override + public Object call() { + return new UnboundedReplayBuffer(16); + } + }; + + /** + * Given a connectable observable factory, it multicasts over the generated + * ConnectableObservable via a selector function. + * @param connectableFactory + * @param selector + * @return + */ + public static Observable multicastSelector( + final Func0> connectableFactory, + final Func1, ? extends Observable> selector) { + return Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber child) { + ConnectableObservable co; + Observable observable; + try { + co = connectableFactory.call(); + observable = selector.call(co); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + child.onError(e); + return; + } + + observable.subscribe(child); + + co.connect(new Action1() { + @Override + public void call(Subscription t) { + child.add(t); + } + }); + } + }); + } + + /** + * Child Subscribers will observe the events of the ConnectableObservable on the + * specified scheduler. + * @param co + * @param scheduler + * @return + */ + public static ConnectableObservable observeOn(final ConnectableObservable co, final Scheduler scheduler) { + final Observable observable = co.observeOn(scheduler); + OnSubscribe onSubscribe = new OnSubscribe() { + @Override + public void call(final Subscriber child) { + // apply observeOn and prevent calling onStart() again + observable.unsafeSubscribe(new Subscriber(child) { + @Override + public void onNext(T t) { + child.onNext(t); + } + @Override + public void onError(Throwable e) { + child.onError(e); + } + @Override + public void onCompleted() { + child.onCompleted(); + } + }); + } + }; + return new ConnectableObservable(onSubscribe) { + @Override + public void connect(Action1 connection) { + co.connect(connection); + } + }; + } + + /** + * Creates a replaying ConnectableObservable with an unbounded buffer. + * @param source + * @return + */ + @SuppressWarnings("unchecked") + public static ConnectableObservable create(Observable source) { + return create(source, DEFAULT_UNBOUNDED_FACTORY); + } + + /** + * Creates a replaying ConnectableObservable with a size bound buffer. + * @param source + * @param bufferSize + * @return + */ + public static ConnectableObservable create(Observable source, + final int bufferSize) { + if (bufferSize == Integer.MAX_VALUE) { + return create(source); + } + return create(source, new Func0>() { + @Override + public ReplayBuffer call() { + return new SizeBoundReplayBuffer(bufferSize); + } + }); } /** - * Creates a subject whose client observers will observe events - * propagated through the given wrapped subject. - * @param the element type - * @param subject the subject to wrap - * @param scheduler the target scheduler - * @return the created subject + * Creates a replaying ConnectableObservable with a time bound buffer. + * @param source + * @param maxAge + * @param unit + * @param scheduler + * @return */ - public static Subject createScheduledSubject(Subject subject, Scheduler scheduler) { - final Observable observedOn = subject.observeOn(scheduler); - SubjectWrapper s = new SubjectWrapper(new OnSubscribe() { + public static ConnectableObservable create(Observable source, + long maxAge, TimeUnit unit, Scheduler scheduler) { + return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE); + } + /** + * Creates a replaying ConnectableObservable with a size and time bound buffer. + * @param source + * @param maxAge + * @param unit + * @param scheduler + * @param bufferSize + * @return + */ + public static ConnectableObservable create(Observable source, + long maxAge, TimeUnit unit, final Scheduler scheduler, final int bufferSize) { + final long maxAgeInMillis = unit.toMillis(maxAge); + return create(source, new Func0>() { @Override - public void call(Subscriber o) { - subscriberOf(observedOn).call(o); + public ReplayBuffer call() { + return new SizeAndTimeBoundReplayBuffer(bufferSize, maxAgeInMillis, scheduler); } - - }, subject); - return s; + }); } /** - * Return an OnSubscribeFunc which delegates the subscription to the given observable. - * - * @param the value type - * @param target the target observable - * @return the function that delegates the subscription to the target + * Creates a OperatorReplay instance to replay values of the given source observable. + * @param source the source observable + * @param bufferFactory the factory to instantiate the appropriate buffer when the observable becomes active + * @return the connectable observable */ - public static OnSubscribe subscriberOf(final Observable target) { - return new OnSubscribe() { + static ConnectableObservable create(Observable source, + final Func0> bufferFactory) { + // the current connection to source needs to be shared between the operator and its onSubscribe call + final AtomicReference> curr = new AtomicReference>(); + OnSubscribe onSubscribe = new OnSubscribe() { @Override - public void call(Subscriber t1) { - target.unsafeSubscribe(t1); + public void call(Subscriber child) { + // concurrent connection/disconnection may change the state, + // we loop to be atomic while the child subscribes + for (;;) { + // get the current subscriber-to-source + ReplaySubscriber r = curr.get(); + // if there isn't one + if (r == null) { + // create a new subscriber to source + ReplaySubscriber u = new ReplaySubscriber(curr, bufferFactory.call()); + // perform extra initialization to avoid 'this' to escape during construction + u.init(); + // let's try setting it as the current subscriber-to-source + if (!curr.compareAndSet(r, u)) { + // didn't work, maybe someone else did it or the current subscriber + // to source has just finished + continue; + } + // we won, let's use it going onwards + r = u; + } + + // create the backpressure-managing producer for this child + InnerProducer inner = new InnerProducer(r, child); + // we try to add it to the array of producers + // if it fails, no worries because we will still have its buffer + // so it is going to replay it for us + r.add(inner); + // the producer has been registered with the current subscriber-to-source so + // at least it will receive the next terminal event + child.add(inner); + // setting the producer will trigger the first request to be considered by + // the subscriber-to-source. + child.setProducer(inner); + break; + } } }; + return new OperatorReplay(onSubscribe, source, curr, bufferFactory); + } + private OperatorReplay(OnSubscribe onSubscribe, Observable source, + final AtomicReference> current, + final Func0> bufferFactory) { + super(onSubscribe); + this.source = source; + this.current = current; + this.bufferFactory = bufferFactory; + } + + @Override + public void connect(Action1 connection) { + boolean doConnect = false; + ReplaySubscriber ps; + // we loop because concurrent connect/disconnect and termination may change the state + for (;;) { + // retrieve the current subscriber-to-source instance + ps = current.get(); + // if there is none yet or the current has unsubscribed + if (ps == null || ps.isUnsubscribed()) { + // create a new subscriber-to-source + ReplaySubscriber u = new ReplaySubscriber(current, bufferFactory.call()); + // initialize out the constructor to avoid 'this' to escape + u.init(); + // try setting it as the current subscriber-to-source + if (!current.compareAndSet(ps, u)) { + // did not work, perhaps a new subscriber arrived + // and created a new subscriber-to-source as well, retry + continue; + } + ps = u; + } + // if connect() was called concurrently, only one of them should actually + // connect to the source + doConnect = !ps.shouldConnect.get() && ps.shouldConnect.compareAndSet(false, true); + break; + } + /* + * Notify the callback that we have a (new) connection which it can unsubscribe + * but since ps is unique to a connection, multiple calls to connect() will return the + * same Subscription and even if there was a connect-disconnect-connect pair, the older + * references won't disconnect the newer connection. + * Synchronous source consumers have the opportunity to disconnect via unsubscribe on the + * Subscription as unsafeSubscribe may never return in its own. + * + * Note however, that asynchronously disconnecting a running source might leave + * child-subscribers without any terminal event; ReplaySubject does not have this + * issue because the unsubscription was always triggered by the child-subscribers + * themselves. + */ + connection.call(ps); + if (doConnect) { + source.unsafeSubscribe(ps); + } } + + @SuppressWarnings("rawtypes") + static final class ReplaySubscriber extends Subscriber implements Subscription { + /** Holds notifications from upstream. */ + final ReplayBuffer buffer; + /** The notification-lite factory. */ + final NotificationLite nl; + /** Contains either an onCompleted or an onError token from upstream. */ + boolean done; + + /** Indicates an empty array of inner producers. */ + static final InnerProducer[] EMPTY = new InnerProducer[0]; + /** Indicates a terminated ReplaySubscriber. */ + static final InnerProducer[] TERMINATED = new InnerProducer[0]; + + /** Tracks the subscribed producers. */ + final AtomicReference producers; + /** + * Atomically changed from false to true by connect to make sure the + * connection is only performed by one thread. + */ + final AtomicBoolean shouldConnect; + + /** Guarded by this. */ + boolean emitting; + /** Guarded by this. */ + boolean missed; + + + /** Contains the maximum element index the child Subscribers requested so far. Accessed while emitting is true. */ + long maxChildRequested; + /** Counts the outstanding upstream requests until the producer arrives. */ + long maxUpstreamRequested; + /** The upstream producer. */ + volatile Producer producer; + + public ReplaySubscriber(AtomicReference> current, + ReplayBuffer buffer) { + this.buffer = buffer; + + this.nl = NotificationLite.instance(); + this.producers = new AtomicReference(EMPTY); + this.shouldConnect = new AtomicBoolean(); + // make sure the source doesn't produce values until the child subscribers + // expressed their request amounts + this.request(0); + } + /** Should be called after the constructor finished to setup nulling-out the current reference. */ + void init() { + add(Subscriptions.create(new Action0() { + @Override + public void call() { + ReplaySubscriber.this.producers.getAndSet(TERMINATED); + // unlike OperatorPublish, we can't null out the terminated so + // late subscribers can still get replay + // current.compareAndSet(ReplaySubscriber.this, null); + // we don't care if it fails because it means the current has + // been replaced in the meantime + } + })); + } + /** + * Atomically try adding a new InnerProducer to this Subscriber or return false if this + * Subscriber was terminated. + * @param producer the producer to add + * @return true if succeeded, false otherwise + */ + boolean add(InnerProducer producer) { + if (producer == null) { + throw new NullPointerException(); + } + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // get the current producer array + InnerProducer[] c = producers.get(); + // if this subscriber-to-source reached a terminal state by receiving + // an onError or onCompleted, just refuse to add the new producer + if (c == TERMINATED) { + return false; + } + // we perform a copy-on-write logic + int len = c.length; + InnerProducer[] u = new InnerProducer[len + 1]; + System.arraycopy(c, 0, u, 0, len); + u[len] = producer; + // try setting the producers array + if (producers.compareAndSet(c, u)) { + return true; + } + // if failed, some other operation succeded (another add, remove or termination) + // so retry + } + } + + /** + * Atomically removes the given producer from the producers array. + * @param producer the producer to remove + */ + void remove(InnerProducer producer) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // let's read the current producers array + InnerProducer[] c = producers.get(); + // if it is either empty or terminated, there is nothing to remove so we quit + if (c == EMPTY || c == TERMINATED) { + return; + } + // let's find the supplied producer in the array + // although this is O(n), we don't expect too many child subscribers in general + int j = -1; + int len = c.length; + for (int i = 0; i < len; i++) { + if (c[i].equals(producer)) { + j = i; + break; + } + } + // we didn't find it so just quit + if (j < 0) { + return; + } + // we do copy-on-write logic here + InnerProducer[] u; + // we don't create a new empty array if producer was the single inhabitant + // but rather reuse an empty array + if (len == 1) { + u = EMPTY; + } else { + // otherwise, create a new array one less in size + u = new InnerProducer[len - 1]; + // copy elements being before the given producer + System.arraycopy(c, 0, u, 0, j); + // copy elements being after the given producer + System.arraycopy(c, j + 1, u, j, len - j - 1); + } + // try setting this new array as + if (producers.compareAndSet(c, u)) { + return; + } + // if we failed, it means something else happened + // (a concurrent add/remove or termination), we need to retry + } + } + + @Override + public void setProducer(Producer p) { + Producer p0 = producer; + if (p0 != null) { + throw new IllegalStateException("Only a single producer can be set on a Subscriber."); + } + producer = p; + manageRequests(); + replay(); + } + + @Override + public void onNext(T t) { + if (!done) { + buffer.next(t); + replay(); + } + } + @Override + public void onError(Throwable e) { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + try { + buffer.error(e); + replay(); + } finally { + unsubscribe(); // expectation of testIssue2191 + } + } + } + @Override + public void onCompleted() { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + try { + buffer.complete(); + replay(); + } finally { + unsubscribe(); + } + } + } + + /** + * Coordinates the request amounts of various child Subscribers. + */ + void manageRequests() { + // if the upstream has completed, no more requesting is possible + if (isUnsubscribed()) { + return; + } + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + } + for (;;) { + // if the upstream has completed, no more requesting is possible + if (isUnsubscribed()) { + return; + } + + @SuppressWarnings("unchecked") + InnerProducer[] a = producers.get(); + + long ri = maxChildRequested; + long maxTotalRequests = 0; + + for (InnerProducer rp : a) { + maxTotalRequests = Math.max(maxTotalRequests, rp.totalRequested.get()); + } + + long ur = maxUpstreamRequested; + Producer p = producer; + long diff = maxTotalRequests - ri; + if (diff != 0) { + maxChildRequested = maxTotalRequests; + if (p != null) { + if (ur != 0L) { + maxUpstreamRequested = 0L; + p.request(ur + diff); + } else { + p.request(diff); + } + } else { + // collect upstream request amounts until there is a producer for them + long u = ur + diff; + if (u < 0) { + u = Long.MAX_VALUE; + } + maxUpstreamRequested = u; + } + } else + // if there were outstanding upstream requests and we have a producer + if (ur != 0L && p != null) { + maxUpstreamRequested = 0L; + // fire the accumulated requests + p.request(ur); + } + + synchronized (this) { + if (!missed) { + emitting = false; + return; + } + missed = false; + } + } + } + + /** + * Tries to replay the buffer contents to all known subscribers. + */ + void replay() { + @SuppressWarnings("unchecked") + InnerProducer[] a = producers.get(); + for (InnerProducer rp : a) { + buffer.replay(rp); + } + } + } + /** + * A Producer and Subscription that manages the request and unsubscription state of a + * child subscriber in thread-safe manner. + * We use AtomicLong as a base class to save on extra allocation of an AtomicLong and also + * save the overhead of the AtomicIntegerFieldUpdater. + * @param the value type + */ + static final class InnerProducer extends AtomicLong implements Producer, Subscription { + /** */ + private static final long serialVersionUID = -4453897557930727610L; + /** + * The parent subscriber-to-source used to allow removing the child in case of + * child unsubscription. + */ + final ReplaySubscriber parent; + /** The actual child subscriber. */ + final Subscriber child; + /** + * Holds an object that represents the current location in the buffer. + * Guarded by the emitter loop. + */ + Object index; + /** + * Keeps the sum of all requested amounts. + */ + final AtomicLong totalRequested; + /** Indicates an emission state. Guarded by this. */ + boolean emitting; + /** Indicates a missed update. Guarded by this. */ + boolean missed; + /** + * Indicates this child has been unsubscribed: the state is swapped in atomically and + * will prevent the dispatch() to emit (too many) values to a terminated child subscriber. + */ + static final long UNSUBSCRIBED = Long.MIN_VALUE; + + public InnerProducer(ReplaySubscriber parent, Subscriber child) { + this.parent = parent; + this.child = child; + this.totalRequested = new AtomicLong(); + } + + @Override + public void request(long n) { + // ignore negative requests + if (n < 0) { + return; + } + // In general, RxJava doesn't prevent concurrent requests (with each other or with + // an unsubscribe) so we need a CAS-loop, but we need to handle + // request overflow and unsubscribed/not requested state as well. + for (;;) { + // get the current request amount + long r = get(); + // if child called unsubscribe() do nothing + if (r == UNSUBSCRIBED) { + return; + } + // ignore zero requests except any first that sets in zero + if (r >= 0L && n == 0) { + return; + } + // otherwise, increase the request count + long u = r + n; + // and check for long overflow + if (u < 0) { + // cap at max value, which is essentially unlimited + u = Long.MAX_VALUE; + } + // try setting the new request value + if (compareAndSet(r, u)) { + // increment the total request counter + addTotalRequested(n); + // if successful, notify the parent dispacher this child can receive more + // elements + parent.manageRequests(); + + parent.buffer.replay(this); + return; + } + // otherwise, someone else changed the state (perhaps a concurrent + // request or unsubscription so retry + } + } + + /** + * Increments the total requested amount. + * @param n the additional request amount + */ + void addTotalRequested(long n) { + for (;;) { + long r = totalRequested.get(); + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + if (totalRequested.compareAndSet(r, u)) { + return; + } + } + } + + /** + * Indicate that values have been emitted to this child subscriber by the dispatch() method. + * @param n the number of items emitted + * @return the updated request value (may indicate how much can be produced or a terminal state) + */ + public long produced(long n) { + // we don't allow producing zero or less: it would be a bug in the operator + if (n <= 0) { + throw new IllegalArgumentException("Cant produce zero or less"); + } + for (;;) { + // get the current request value + long r = get(); + // if the child has unsubscribed, simply return and indicate this + if (r == UNSUBSCRIBED) { + return UNSUBSCRIBED; + } + // reduce the requested amount + long u = r - n; + // if the new amount is less than zero, we have a bug in this operator + if (u < 0) { + throw new IllegalStateException("More produced (" + n + ") than requested (" + r + ")"); + } + // try updating the request value + if (compareAndSet(r, u)) { + // and return the udpated value + return u; + } + // otherwise, some concurrent activity happened and we need to retry + } + } + + @Override + public boolean isUnsubscribed() { + return get() == UNSUBSCRIBED; + } + @Override + public void unsubscribe() { + long r = get(); + // let's see if we are unsubscribed + if (r != UNSUBSCRIBED) { + // if not, swap in the terminal state, this is idempotent + // because other methods using CAS won't overwrite this value, + // concurrent calls to unsubscribe will atomically swap in the same + // terminal value + r = getAndSet(UNSUBSCRIBED); + // and only one of them will see a non-terminated value before the swap + if (r != UNSUBSCRIBED) { + // remove this from the parent + parent.remove(this); + // After removal, we might have unblocked the other child subscribers: + // let's assume this child had 0 requested before the unsubscription while + // the others had non-zero. By removing this 'blocking' child, the others + // are now free to receive events + parent.manageRequests(); + } + } + } + /** + * Convenience method to auto-cast the index object. + * @return + */ + @SuppressWarnings("unchecked") + U index() { + return (U)index; + } + } /** - * A subject that wraps another subject. + * The interface for interacting with various buffering logic. + * * @param the value type */ - public static final class SubjectWrapper extends Subject { - /** The wrapped subject. */ - final Subject subject; + interface ReplayBuffer { + /** + * Adds a regular value to the buffer. + * @param value + */ + void next(T value); + /** + * Adds a terminal exception to the buffer + * @param e + */ + void error(Throwable e); + /** + * Adds a completion event to the buffer + */ + void complete(); + /** + * Tries to replay the buffered values to the + * subscriber inside the output if there + * is new value and requests available at the + * same time. + * @param output + */ + void replay(InnerProducer output); + } + + /** + * Holds an unbounded list of events. + * + * @param the value type + */ + static final class UnboundedReplayBuffer extends ArrayList implements ReplayBuffer { + /** */ + private static final long serialVersionUID = 7063189396499112664L; + final NotificationLite nl; + /** The total number of events in the buffer. */ + volatile int size; + + public UnboundedReplayBuffer(int capacityHint) { + super(capacityHint); + nl = NotificationLite.instance(); + } + @Override + public void next(T value) { + add(nl.next(value)); + size++; + } - public SubjectWrapper(OnSubscribe func, Subject subject) { - super(func); - this.subject = subject; + @Override + public void error(Throwable e) { + add(nl.error(e)); + size++; } @Override - public void onNext(T args) { - subject.onNext(args); + public void complete() { + add(nl.completed()); + size++; } @Override - public void onError(Throwable e) { - subject.onError(e); + public void replay(InnerProducer output) { + synchronized (output) { + if (output.emitting) { + output.missed = true; + return; + } + output.emitting = true; + } + for (;;) { + if (output.isUnsubscribed()) { + return; + } + int sourceIndex = size; + + Integer destIndexObject = output.index(); + int destIndex = destIndexObject != null ? destIndexObject.intValue() : 0; + + long r = output.get(); + long r0 = r; + long e = 0L; + + while (r != 0L && destIndex < sourceIndex) { + Object o = get(destIndex); + if (nl.accept(output.child, o)) { + return; + } + if (output.isUnsubscribed()) { + return; + } + destIndex++; + r--; + e++; + } + if (e != 0L) { + output.index = destIndex; + if (r0 != Long.MAX_VALUE) { + output.produced(e); + } + } + + synchronized (output) { + if (!output.missed) { + output.emitting = false; + return; + } + output.missed = false; + } + } + } + } + + /** + * Represents a node in a bounded replay buffer's linked list. + * + * @param the contained value type + */ + static final class Node extends AtomicReference { + /** */ + private static final long serialVersionUID = 245354315435971818L; + final Object value; + public Node(Object value) { + this.value = value; + } + } + + /** + * Base class for bounded buffering with options to specify an + * enter and leave transforms and custom truncation behavior. + * + * @param the value type + */ + static class BoundedReplayBuffer extends AtomicReference implements ReplayBuffer { + /** */ + private static final long serialVersionUID = 2346567790059478686L; + final NotificationLite nl; + + Node tail; + int size; + + public BoundedReplayBuffer() { + nl = NotificationLite.instance(); + Node n = new Node(null); + tail = n; + set(n); + } + + /** + * Add a new node to the linked list. + * @param n + */ + final void addLast(Node n) { + tail.set(n); + tail = n; + size++; + } + /** + * Remove the first node from the linked list. + */ + final void removeFirst() { + Node head = get(); + Node next = head.get(); + if (next == null) { + throw new IllegalStateException("Empty list!"); + } + size--; + // can't just move the head because it would retain the very first value + // can't null out the head's value because of late replayers would see null + setFirst(next.get()); + } + final void removeSome(int n) { + Node head = get(); + while (n > 0) { + head = head.get(); + n--; + size--; + } + + setFirst(head.get()); + } + /** + * Arranges the given node is the new head from now on. + * @param n + */ + final void setFirst(Node n) { + Node newHead = new Node(null); + newHead.lazySet(n); + if (n == null) { + tail = newHead; + } + set(newHead); + } + + @Override + public final void next(T value) { + Object o = enterTransform(nl.next(value)); + Node n = new Node(o); + addLast(n); + truncate(); } @Override - public void onCompleted() { - subject.onCompleted(); + public final void error(Throwable e) { + Object o = enterTransform(nl.error(e)); + Node n = new Node(o); + addLast(n); + truncateFinal(); } @Override - public boolean hasObservers() { - return this.subject.hasObservers(); + public final void complete() { + Object o = enterTransform(nl.completed()); + Node n = new Node(o); + addLast(n); + truncateFinal(); + } + + @Override + public final void replay(InnerProducer output) { + synchronized (output) { + if (output.emitting) { + output.missed = true; + return; + } + output.emitting = true; + } + for (;;) { + if (output.isUnsubscribed()) { + return; + } + + long r = output.get(); + long r0 = r; + long e = 0L; + + Node node = output.index(); + if (node == null) { + node = get(); + output.index = node; + } + + while (r != 0) { + Node v = node.get(); + if (v != null) { + Object o = leaveTransform(v.value); + if (nl.accept(output.child, o)) { + output.index = null; + return; + } + e++; + node = v; + } else { + break; + } + if (output.isUnsubscribed()) { + return; + } + } + + if (e != 0L) { + output.index = node; + if (r0 != Long.MAX_VALUE) { + output.produced(e); + } + } + + synchronized (output) { + if (!output.missed) { + output.emitting = false; + return; + } + output.missed = false; + } + } + + } + + /** + * Override this to wrap the NotificationLite object into a + * container to be used later by truncate. + * @param value + * @return + */ + Object enterTransform(Object value) { + return value; + } + /** + * Override this to unwrap the transformed value into a + * NotificationLite object. + * @param value + * @return + */ + Object leaveTransform(Object value) { + return value; + } + /** + * Override this method to truncate a non-terminated buffer + * based on its current properties. + */ + void truncate() { + + } + /** + * Override this method to truncate a terminated buffer + * based on its properties (i.e., truncate but the very last node). + */ + void truncateFinal() { + + } + /* test */ final void collect(Collection output) { + Node n = get(); + for (;;) { + Node next = n.get(); + if (next != null) { + Object o = next.value; + Object v = leaveTransform(o); + if (nl.isCompleted(v) || nl.isError(v)) { + break; + } + output.add(nl.getValue(v)); + n = next; + } else { + break; + } + } + } + /* test */ boolean hasError() { + return tail.value != null && nl.isError(leaveTransform(tail.value)); + } + /* test */ boolean hasCompleted() { + return tail.value != null && nl.isCompleted(leaveTransform(tail.value)); + } + } + + /** + * A bounded replay buffer implementation with size limit only. + * + * @param the value type + */ + static final class SizeBoundReplayBuffer extends BoundedReplayBuffer { + /** */ + private static final long serialVersionUID = -5898283885385201806L; + + final int limit; + public SizeBoundReplayBuffer(int limit) { + this.limit = limit; + } + + @Override + void truncate() { + // overflow can be at most one element + if (size > limit) { + removeFirst(); + } + } + + // no need for final truncation because values are truncated one by one + } + + /** + * Size and time bound replay buffer. + * + * @param the buffered value type + */ + static final class SizeAndTimeBoundReplayBuffer extends BoundedReplayBuffer { + /** */ + private static final long serialVersionUID = 3457957419649567404L; + final Scheduler scheduler; + final long maxAgeInMillis; + final int limit; + public SizeAndTimeBoundReplayBuffer(int limit, long maxAgeInMillis, Scheduler scheduler) { + this.scheduler = scheduler; + this.limit = limit; + this.maxAgeInMillis = maxAgeInMillis; + } + + @Override + Object enterTransform(Object value) { + return new Timestamped(scheduler.now(), value); + } + + @Override + Object leaveTransform(Object value) { + return ((Timestamped)value).getValue(); + } + + @Override + void truncate() { + long timeLimit = scheduler.now() - maxAgeInMillis; + + Node head = get(); + Node next = head.get(); + + int e = 0; + for (;;) { + if (next != null) { + if (size > limit) { + e++; + size--; + next = next.get(); + } else { + Timestamped v = (Timestamped)next.value; + if (v.getTimestampMillis() <= timeLimit) { + e++; + size--; + next = next.get(); + } else { + break; + } + } + } else { + break; + } + } + if (e != 0) { + setFirst(next); + } + } + @Override + void truncateFinal() { + long timeLimit = scheduler.now() - maxAgeInMillis; + + Node head = get(); + Node next = head.get(); + + int e = 0; + for (;;) { + if (next != null && size > 1) { + Timestamped v = (Timestamped)next.value; + if (v.getTimestampMillis() <= timeLimit) { + e++; + size--; + next = next.get(); + } else { + break; + } + } else { + break; + } + } + if (e != 0) { + setFirst(next); + } } } -} \ No newline at end of file +} diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index a5ff85864d..5c31503da4 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -16,33 +16,27 @@ package rx.internal.operators; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.notNull; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import java.util.*; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.*; -import org.junit.Test; +import org.junit.*; import org.mockito.InOrder; +import rx.*; +import rx.Scheduler.Worker; import rx.Observable; import rx.Observer; -import rx.Scheduler; -import rx.Scheduler.Worker; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.functions.*; +import rx.internal.operators.OperatorReplay.BoundedReplayBuffer; +import rx.internal.operators.OperatorReplay.Node; +import rx.internal.operators.OperatorReplay.SizeAndTimeBoundReplayBuffer; import rx.observables.ConnectableObservable; -import rx.schedulers.TestScheduler; +import rx.observers.TestSubscriber; +import rx.schedulers.*; import rx.subjects.PublishSubject; public class OperatorReplayTest { @@ -739,4 +733,132 @@ public boolean isUnsubscribed() { } } + @Test + public void testBoundedReplayBuffer() { + BoundedReplayBuffer buf = new BoundedReplayBuffer(); + buf.addLast(new Node(1)); + buf.addLast(new Node(2)); + buf.addLast(new Node(3)); + buf.addLast(new Node(4)); + buf.addLast(new Node(5)); + + List values = new ArrayList(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5)); + buf.addLast(new Node(6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + + } + + @Test + public void testTimedAndSizedTruncation() { + TestScheduler test = Schedulers.test(); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, test); + List values = new ArrayList(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + } + + @Test + public void testBackpressure() { + final AtomicLong requested = new AtomicLong(); + Observable source = Observable.range(1, 1000) + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requested.addAndGet(t); + } + }); + ConnectableObservable co = source.replay(); + + TestSubscriber ts1 = TestSubscriber.create(10); + TestSubscriber ts2 = TestSubscriber.create(90); + + co.subscribe(ts1); + co.subscribe(ts2); + + ts2.requestMore(10); + + co.connect(); + + ts1.assertValueCount(10); + ts1.assertNoTerminalEvent(); + + ts2.assertValueCount(100); + ts2.assertNoTerminalEvent(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void testBackpressureBounded() { + final AtomicLong requested = new AtomicLong(); + Observable source = Observable.range(1, 1000) + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requested.addAndGet(t); + } + }); + ConnectableObservable co = source.replay(50); + + TestSubscriber ts1 = TestSubscriber.create(10); + TestSubscriber ts2 = TestSubscriber.create(90); + + co.subscribe(ts1); + co.subscribe(ts2); + + ts2.requestMore(10); + + co.connect(); + + ts1.assertValueCount(10); + ts1.assertNoTerminalEvent(); + + ts2.assertValueCount(100); + ts2.assertNoTerminalEvent(); + + Assert.assertEquals(100, requested.get()); + } } \ No newline at end of file From 46f9138f509f22be61d435cfb79335396fc92c48 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 23 Jun 2015 18:25:41 +0200 Subject: [PATCH 317/857] No need to allocate a new head node. --- .../rx/internal/operators/OperatorReplay.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index 77f19edf32..2989f50b9e 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -897,9 +897,9 @@ final void removeFirst() { size--; // can't just move the head because it would retain the very first value // can't null out the head's value because of late replayers would see null - setFirst(next.get()); + setFirst(next); } - final void removeSome(int n) { + /* test */ final void removeSome(int n) { Node head = get(); while (n > 0) { head = head.get(); @@ -907,19 +907,14 @@ final void removeSome(int n) { size--; } - setFirst(head.get()); + setFirst(head); } /** * Arranges the given node is the new head from now on. * @param n */ final void setFirst(Node n) { - Node newHead = new Node(null); - newHead.lazySet(n); - if (n == null) { - tail = newHead; - } - set(newHead); + set(n); } @Override @@ -1119,8 +1114,8 @@ Object leaveTransform(Object value) { void truncate() { long timeLimit = scheduler.now() - maxAgeInMillis; - Node head = get(); - Node next = head.get(); + Node prev = get(); + Node next = prev.get(); int e = 0; for (;;) { @@ -1128,12 +1123,14 @@ void truncate() { if (size > limit) { e++; size--; + prev = next; next = next.get(); } else { Timestamped v = (Timestamped)next.value; if (v.getTimestampMillis() <= timeLimit) { e++; size--; + prev = next; next = next.get(); } else { break; @@ -1144,15 +1141,15 @@ void truncate() { } } if (e != 0) { - setFirst(next); + setFirst(prev); } } @Override void truncateFinal() { long timeLimit = scheduler.now() - maxAgeInMillis; - Node head = get(); - Node next = head.get(); + Node prev = get(); + Node next = prev.get(); int e = 0; for (;;) { @@ -1161,6 +1158,7 @@ void truncateFinal() { if (v.getTimestampMillis() <= timeLimit) { e++; size--; + prev = next; next = next.get(); } else { break; @@ -1170,7 +1168,7 @@ void truncateFinal() { } } if (e != 0) { - setFirst(next); + setFirst(prev); } } } From 8951a339d427efbc8b30a9f07319a22ed53e440b Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 23 Jun 2015 23:05:51 +0200 Subject: [PATCH 318/857] CompositeException extra NPE protection --- .../java/rx/exceptions/CompositeException.java | 17 ++++++++++++----- .../rx/exceptions/CompositeExceptionTest.java | 13 +++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/exceptions/CompositeException.java b/src/main/java/rx/exceptions/CompositeException.java index 2084ef4607..7d6e37e8b9 100644 --- a/src/main/java/rx/exceptions/CompositeException.java +++ b/src/main/java/rx/exceptions/CompositeException.java @@ -49,12 +49,19 @@ public final class CompositeException extends RuntimeException { public CompositeException(String messagePrefix, Collection errors) { Set deDupedExceptions = new LinkedHashSet(); List _exceptions = new ArrayList(); - for (Throwable ex : errors) { - if (ex instanceof CompositeException) { - deDupedExceptions.addAll(((CompositeException) ex).getExceptions()); - } else { - deDupedExceptions.add(ex); + 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); diff --git a/src/test/java/rx/exceptions/CompositeExceptionTest.java b/src/test/java/rx/exceptions/CompositeExceptionTest.java index cc4a7e0b03..5fadadd42c 100644 --- a/src/test/java/rx/exceptions/CompositeExceptionTest.java +++ b/src/test/java/rx/exceptions/CompositeExceptionTest.java @@ -165,4 +165,17 @@ private static Throwable getRootCause(Throwable ex) { } } } + + @Test + public void testNullCollection() { + CompositeException composite = new CompositeException(null); + composite.getCause(); + composite.printStackTrace(); + } + @Test + public void testNullElement() { + CompositeException composite = new CompositeException(Arrays.asList((Throwable)null)); + composite.getCause(); + composite.printStackTrace(); + } } \ No newline at end of file From 070e42adcdba836719ebb0510d746b348685db94 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 24 Jun 2015 13:32:57 +0200 Subject: [PATCH 319/857] Reduce test failure likelihood of testMultiThreadedWithNPEinMiddle --- src/test/java/rx/observers/SerializedObserverTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/rx/observers/SerializedObserverTest.java b/src/test/java/rx/observers/SerializedObserverTest.java index 1592ea007d..b469c131d4 100644 --- a/src/test/java/rx/observers/SerializedObserverTest.java +++ b/src/test/java/rx/observers/SerializedObserverTest.java @@ -674,6 +674,7 @@ public TestMultiThreadedObservable(String... values) { @Override public void call(final Subscriber observer) { + final NullPointerException npe = new NullPointerException(); System.out.println("TestMultiThreadedObservable subscribed to ..."); t = new Thread(new Runnable() { @@ -694,7 +695,7 @@ public void run() { System.out.println("TestMultiThreadedObservable onNext: " + s + " on thread " + Thread.currentThread().getName()); if (s == null) { // force an error - throw new NullPointerException(); + throw npe; } else { // allow the exception to queue up int sleep = (fj % 3) * 10; From cb9d1eb676c22aedf89dd99879a6df724d2aaaa4 Mon Sep 17 00:00:00 2001 From: Lalit Maganti Date: Wed, 24 Jun 2015 00:32:08 +0100 Subject: [PATCH 320/857] single: add toSingle method to Observable * closes ReactiveX/RxJava#3038 * this method allows an observable which is guaranteed to return exactly one item to be converted to a Single * NOTE: the semantics of this function are very similar to that of single * i.e. errors are passed through, more than one item results in an IllegalArgumentException, completion without emission results in a NoSuchElementException and exactly one item is passed through the onSuccess method of SingleSubscriber --- src/main/java/rx/Observable.java | 24 ++++- .../internal/operators/OnSubscribeSingle.java | 89 +++++++++++++++++++ .../operators/OnSubscribeSingleTest.java | 73 +++++++++++++++ 3 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OnSubscribeSingle.java create mode 100644 src/test/java/rx/internal/operators/OnSubscribeSingleTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 776026fdb4..3f27cb0a83 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -195,8 +195,28 @@ public Observable compose(Transformer transformer public interface Transformer extends Func1, Observable> { // cover for generics insanity } - - + + /** + * Returns a Single that emits the single item emitted by the source Observable, if that Observable + * emits only a single item. If the source Observable emits more than one item or no items, notify of an + * {@code IllegalArgumentException} or {@code NoSuchElementException} respectively. + *

+ *

+ *
Scheduler:
+ *
{@code toSingle} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @return a Single that emits the single item emitted by the source Observable + * @throws IllegalArgumentException + * if the source observable emits more than one item + * @throws NoSuchElementException + * if the source observable emits no items + */ + @Experimental + public Single toSingle() { + return new Single(OnSubscribeSingle.create(this)); + } + /* ********************************************************************************************************* * Operators Below Here diff --git a/src/main/java/rx/internal/operators/OnSubscribeSingle.java b/src/main/java/rx/internal/operators/OnSubscribeSingle.java new file mode 100644 index 0000000000..63d4d0a49a --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeSingle.java @@ -0,0 +1,89 @@ +/** + * 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 java.util.NoSuchElementException; + +/** + * Allows conversion of an Observable to a Single ensuring that exactly one item is emitted - no more and no less. + * Also forwards errors as appropriate. + */ +public class OnSubscribeSingle implements Single.OnSubscribe { + + private final Observable observable; + + public OnSubscribeSingle(Observable observable) { + this.observable = observable; + } + + @Override + public void call(final SingleSubscriber child) { + Subscriber parent = new Subscriber() { + private boolean emittedTooMany = false; + private boolean itemEmitted = false; + private T emission = null; + + @Override + public void onStart() { + // We request 2 here since we need 1 for the single and 1 to check that the observable + // doesn't emit more than one item + request(2); + } + + @Override + public void onCompleted() { + if (emittedTooMany) { + // Don't need to do anything here since we already sent an error downstream + } else { + if (itemEmitted) { + child.onSuccess(emission); + } else { + child.onError(new NoSuchElementException("Observable emitted no items")); + } + } + } + + @Override + public void onError(Throwable e) { + child.onError(e); + unsubscribe(); + } + + @Override + public void onNext(T t) { + if (itemEmitted) { + emittedTooMany = true; + child.onError(new IllegalArgumentException("Observable emitted too many elements")); + unsubscribe(); + } else { + itemEmitted = true; + emission = t; + } + } + }; + child.add(parent); + observable.subscribe(parent); + } + + public static OnSubscribeSingle create(Observable observable) { + return new OnSubscribeSingle(observable); + } +} diff --git a/src/test/java/rx/internal/operators/OnSubscribeSingleTest.java b/src/test/java/rx/internal/operators/OnSubscribeSingleTest.java new file mode 100644 index 0000000000..6bc24dbe75 --- /dev/null +++ b/src/test/java/rx/internal/operators/OnSubscribeSingleTest.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.operators; + +import org.junit.Test; +import rx.Observable; +import rx.Single; +import rx.observers.TestSubscriber; + +import java.util.Collections; +import java.util.NoSuchElementException; + +public class OnSubscribeSingleTest { + + @Test + public void testJustSingleItemObservable() { + TestSubscriber subscriber = TestSubscriber.create(); + Single single = Observable.just("Hello World!").toSingle(); + single.subscribe(subscriber); + + subscriber.assertReceivedOnNext(Collections.singletonList("Hello World!")); + } + + @Test + public void testErrorObservable() { + TestSubscriber subscriber = TestSubscriber.create(); + IllegalArgumentException error = new IllegalArgumentException("Error"); + Single single = Observable.error(error).toSingle(); + single.subscribe(subscriber); + + subscriber.assertError(error); + } + + @Test + public void testJustTwoEmissionsObservableThrowsError() { + TestSubscriber subscriber = TestSubscriber.create(); + Single single = Observable.just("First", "Second").toSingle(); + single.subscribe(subscriber); + + subscriber.assertError(IllegalArgumentException.class); + } + + @Test + public void testEmptyObservable() { + TestSubscriber subscriber = TestSubscriber.create(); + Single single = Observable.empty().toSingle(); + single.subscribe(subscriber); + + subscriber.assertError(NoSuchElementException.class); + } + + @Test + public void testRepeatObservableThrowsError() { + TestSubscriber subscriber = TestSubscriber.create(); + Single single = Observable.just("First", "Second").repeat().toSingle(); + single.subscribe(subscriber); + + subscriber.assertError(IllegalArgumentException.class); + } +} From a9f13b7c1cd747ee17a88fcd942bcf595fdf0616 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Tue, 30 Jun 2015 11:32:28 +1000 Subject: [PATCH 321/857] toSingle should use unsafeSubscribe --- .../internal/operators/OnSubscribeSingle.java | 2 +- .../operators/OnSubscribeSingleTest.java | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeSingle.java b/src/main/java/rx/internal/operators/OnSubscribeSingle.java index 63d4d0a49a..27e976e30c 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeSingle.java +++ b/src/main/java/rx/internal/operators/OnSubscribeSingle.java @@ -80,7 +80,7 @@ public void onNext(T t) { } }; child.add(parent); - observable.subscribe(parent); + observable.unsafeSubscribe(parent); } public static OnSubscribeSingle create(Observable observable) { diff --git a/src/test/java/rx/internal/operators/OnSubscribeSingleTest.java b/src/test/java/rx/internal/operators/OnSubscribeSingleTest.java index 6bc24dbe75..8b3dbf910e 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeSingleTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeSingleTest.java @@ -15,14 +15,19 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertFalse; + +import java.util.Collections; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.Test; + import rx.Observable; import rx.Single; +import rx.functions.Action0; import rx.observers.TestSubscriber; -import java.util.Collections; -import java.util.NoSuchElementException; - public class OnSubscribeSingleTest { @Test @@ -70,4 +75,19 @@ public void testRepeatObservableThrowsError() { subscriber.assertError(IllegalArgumentException.class); } + + @Test + public void testShouldUseUnsafeSubscribeInternallyNotSubscribe() { + TestSubscriber subscriber = TestSubscriber.create(); + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Single single = Observable.just("Hello World!").doOnUnsubscribe(new Action0() { + + @Override + public void call() { + unsubscribed.set(true); + }}).toSingle(); + single.unsafeSubscribe(subscriber); + subscriber.assertCompleted(); + assertFalse(unsubscribed.get()); + } } From bd2d5fef9693f7ac86fb72f70c24dae3ed6e60c8 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 1 Jul 2015 14:40:54 +1000 Subject: [PATCH 322/857] add and improve javadoc in Subscriber --- src/main/java/rx/Subscriber.java | 36 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/Subscriber.java b/src/main/java/rx/Subscriber.java index 1767237b25..67ac611e4c 100644 --- a/src/main/java/rx/Subscriber.java +++ b/src/main/java/rx/Subscriber.java @@ -46,20 +46,34 @@ protected Subscriber() { this(null, false); } + /** + * Construct a Subscriber by using another Subscriber for backpressure and + * for holding the subscription list (when this.add(sub) is + * called this will in fact call subscriber.add(sub)). + * + * @param subscriber + * the other Subscriber + */ protected Subscriber(Subscriber subscriber) { this(subscriber, true); } /** - * Construct a Subscriber by using another Subscriber for backpressure and optionally sharing the - * underlying subscriptions list. + * Construct a Subscriber by using another Subscriber for backpressure and + * optionally for holding the subscription list (if + * shareSubscriptions is true then when + * this.add(sub) is called this will in fact call + * subscriber.add(sub)). *

- * To retain the chaining of subscribers, add the created instance to {@code op} via {@link #add}. + * To retain the chaining of subscribers when setting + * shareSubscriptions to false, add the created + * instance to {@code subscriber} via {@link #add}. * * @param subscriber * the other Subscriber * @param shareSubscriptions - * {@code true} to share the subscription list in {@code op} with this instance + * {@code true} to share the subscription list in {@code subscriber} with + * this instance * @since 1.0.6 */ protected Subscriber(Subscriber subscriber, boolean shareSubscriptions) { @@ -158,9 +172,19 @@ private void addToRequested(long n) { } /** - * @warn javadoc description missing - * @warn param producer not described + * If other subscriber is set (by calling constructor + * {@link #Subscriber(Subscriber)} or + * {@link #Subscriber(Subscriber, boolean)}) then this method calls + * setProducer on the other subscriber. If the other subscriber + * is not set and no requests have been made to this subscriber then + * p.request(Long.MAX_VALUE) is called. If the other subscriber + * is not set and some requests have been made to this subscriber then + * p.request(n) is called where n is the accumulated requests + * to this subscriber. + * * @param p + * producer to be used by this subscriber or the other subscriber + * (or recursively its other subscriber) to make requests from */ public void setProducer(Producer p) { long toRequest; From 11a4157a55b2f6dc5071f65a082b1f4a149c88cd Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 9 Jul 2015 08:12:22 -0700 Subject: [PATCH 323/857] Improve toSingle() javadoc (diagram, see also, since-annotation) --- src/main/java/rx/Observable.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 3f27cb0a83..0672f4cb90 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -201,6 +201,7 @@ public interface Transformer extends Func1, Observable> { * emits only a single item. If the source Observable emits more than one item or no items, notify of an * {@code IllegalArgumentException} or {@code NoSuchElementException} respectively. *

+ * *

*
Scheduler:
*
{@code toSingle} does not operate by default on a particular {@link Scheduler}.
@@ -211,6 +212,8 @@ public interface Transformer extends Func1, Observable> { * if the source observable emits more than one item * @throws NoSuchElementException * if the source observable emits no items + * @see ReactiveX documentation: Single + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public Single toSingle() { From a0998b9fb566395f77bffec666690ed260806617 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 6 Jul 2015 08:48:56 +1000 Subject: [PATCH 324/857] add Subscribers.wrap --- .../internal/operators/OnSubscribeDefer.java | 16 +------- .../OnSubscribeDelaySubscription.java | 16 +------- ...ubscribeDelaySubscriptionWithSelector.java | 16 +------- .../internal/operators/OnSubscribeUsing.java | 16 +------- .../operators/OperatorDoOnSubscribe.java | 16 +------- .../operators/OperatorDoOnUnsubscribe.java | 19 +--------- src/main/java/rx/observers/Subscribers.java | 38 +++++++++++++++++++ 7 files changed, 50 insertions(+), 87 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeDefer.java b/src/main/java/rx/internal/operators/OnSubscribeDefer.java index 4a6434140c..23ee937145 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDefer.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDefer.java @@ -19,6 +19,7 @@ import rx.Observable.OnSubscribe; import rx.Subscriber; import rx.functions.Func0; +import rx.observers.Subscribers; /** * Do not create the Observable until an Observer subscribes; create a fresh Observable on each @@ -46,20 +47,7 @@ public void call(final Subscriber s) { s.onError(t); return; } - o.unsafeSubscribe(new Subscriber(s) { - @Override - public void onNext(T t) { - s.onNext(t); - } - @Override - public void onError(Throwable e) { - s.onError(e); - } - @Override - public void onCompleted() { - s.onCompleted(); - } - }); + o.unsafeSubscribe(Subscribers.wrap(s)); } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java index 95036d399e..1876db73a3 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscription.java @@ -21,6 +21,7 @@ import rx.Observable.OnSubscribe; import rx.Scheduler.Worker; import rx.functions.Action0; +import rx.observers.Subscribers; /** * Delays the subscription to the source by the given amount, running on the given scheduler. @@ -49,20 +50,7 @@ public void call(final Subscriber s) { @Override public void call() { if (!s.isUnsubscribed()) { - source.unsafeSubscribe(new Subscriber(s) { - @Override - public void onNext(T t) { - s.onNext(t); - } - @Override - public void onError(Throwable e) { - s.onError(e); - } - @Override - public void onCompleted() { - s.onCompleted(); - } - }); + source.unsafeSubscribe(Subscribers.wrap(s)); } } }, time, unit); diff --git a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java index d6b2f0ad2c..b32179b3f7 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java @@ -18,6 +18,7 @@ import rx.*; import rx.Observable.OnSubscribe; import rx.functions.Func0; +import rx.observers.Subscribers; /** * Delays the subscription until the Observable emits an event. @@ -42,20 +43,7 @@ public void call(final Subscriber child) { @Override public void onCompleted() { // subscribe to actual source - source.unsafeSubscribe(new Subscriber(child) { - @Override - public void onNext(T t) { - child.onNext(t); - } - @Override - public void onError(Throwable e) { - child.onError(e); - } - @Override - public void onCompleted() { - child.onCompleted(); - } - }); + source.unsafeSubscribe(Subscribers.wrap(child)); } @Override diff --git a/src/main/java/rx/internal/operators/OnSubscribeUsing.java b/src/main/java/rx/internal/operators/OnSubscribeUsing.java index 7470a65dc8..14d8d46b7b 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeUsing.java +++ b/src/main/java/rx/internal/operators/OnSubscribeUsing.java @@ -22,6 +22,7 @@ import rx.Observable.OnSubscribe; import rx.exceptions.CompositeException; import rx.functions.*; +import rx.observers.Subscribers; /** * Constructs an observable sequence that depends on a resource object. @@ -68,20 +69,7 @@ public void call(final Subscriber subscriber) { observable = source; try { // start - observable.unsafeSubscribe(new Subscriber(subscriber) { - @Override - public void onNext(T t) { - subscriber.onNext(t); - } - @Override - public void onError(Throwable e) { - subscriber.onError(e); - } - @Override - public void onCompleted() { - subscriber.onCompleted(); - } - }); + observable.unsafeSubscribe(Subscribers.wrap(subscriber)); } catch (Throwable e) { Throwable disposeError = disposeEagerlyIfRequested(disposeOnceOnly); if (disposeError != null) diff --git a/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java b/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java index b7999c2b5c..685cf9ae1b 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnSubscribe.java @@ -18,6 +18,7 @@ import rx.Observable.Operator; import rx.Subscriber; import rx.functions.Action0; +import rx.observers.Subscribers; /** * This operator modifies an {@link rx.Observable} so a given action is invoked when the {@link rx.Observable} is subscribed. @@ -39,19 +40,6 @@ public Subscriber call(final Subscriber child) { subscribe.call(); // Pass through since this operator is for notification only, there is // no change to the stream whatsoever. - return new Subscriber(child) { - @Override - public void onNext(T t) { - child.onNext(t); - } - @Override - public void onError(Throwable e) { - child.onError(e); - } - @Override - public void onCompleted() { - child.onCompleted(); - } - }; + return Subscribers.wrap(child); } } diff --git a/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java b/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java index 396012c2eb..217c46977f 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnUnsubscribe.java @@ -18,6 +18,7 @@ import rx.Observable.Operator; import rx.*; import rx.functions.Action0; +import rx.observers.Subscribers; import rx.subscriptions.Subscriptions; /** @@ -41,22 +42,6 @@ public Subscriber call(final Subscriber child) { // Pass through since this operator is for notification only, there is // no change to the stream whatsoever. - return new Subscriber(child) { - @Override - public void onStart() { - } - @Override - public void onNext(T t) { - child.onNext(t); - } - @Override - public void onError(Throwable e) { - child.onError(e); - } - @Override - public void onCompleted() { - child.onCompleted(); - } - }; + return Subscribers.wrap(child); } } diff --git a/src/main/java/rx/observers/Subscribers.java b/src/main/java/rx/observers/Subscribers.java index c7cffdb46c..1c42aa4b68 100644 --- a/src/main/java/rx/observers/Subscribers.java +++ b/src/main/java/rx/observers/Subscribers.java @@ -17,6 +17,7 @@ import rx.Observer; import rx.Subscriber; +import rx.annotations.Experimental; import rx.exceptions.OnErrorNotImplementedException; import rx.functions.Action0; import rx.functions.Action1; @@ -198,4 +199,41 @@ public final void onNext(T args) { }; } + /** + * Returns a new {@link Subscriber} that passes all events to + * subscriber, has backpressure controlled by + * subscriber and uses the subscription list of + * subscriber when {@link Subscriber#add(rx.Subscription)} is + * called. + * + * @param subscriber + * the Subscriber to wrap. + * + * @return a new Subscriber that passes all events to + * subscriber, has backpressure controlled by + * subscriber and uses subscriber to + * manage unsubscription. + * + */ + @Experimental + public static Subscriber wrap(final Subscriber subscriber) { + return new Subscriber(subscriber) { + + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(T t) { + subscriber.onNext(t); + } + + }; + } } From e22acf63e83e483c875ddb1ab15f21eda2f00d85 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 13 Jul 2015 16:02:22 +1000 Subject: [PATCH 325/857] OperatorSwitch - fix lost requests race condition using ProducerArbiter --- .../rx/internal/operators/OperatorSwitch.java | 150 +++++++----------- .../operators/OperatorSwitchIfEmptyTest.java | 1 - .../operators/OperatorSwitchTest.java | 19 +-- 3 files changed, 63 insertions(+), 107 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSwitch.java b/src/main/java/rx/internal/operators/OperatorSwitch.java index afd35e477d..cbd02e1b58 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitch.java +++ b/src/main/java/rx/internal/operators/OperatorSwitch.java @@ -22,6 +22,7 @@ import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; +import rx.internal.producers.ProducerArbiter; import rx.observers.SerializedSubscriber; import rx.subscriptions.SerialSubscription; @@ -46,7 +47,9 @@ private static final class Holder { public static OperatorSwitch instance() { return (OperatorSwitch)Holder.INSTANCE; } + private OperatorSwitch() { } + @Override public Subscriber> call(final Subscriber child) { SwitchSubscriber sws = new SwitchSubscriber(child); @@ -55,10 +58,12 @@ public Subscriber> call(final Subscriber extends Subscriber> { - final SerializedSubscriber s; + final SerializedSubscriber serializedChild; final SerialSubscription ssub; final Object guard = new Object(); final NotificationLite nl = NotificationLite.instance(); + final ProducerArbiter arbiter; + /** Guarded by guard. */ int index; /** Guarded by guard. */ @@ -70,50 +75,19 @@ private static final class SwitchSubscriber extends Subscriber currentSubscriber; - public SwitchSubscriber(Subscriber child) { - s = new SerializedSubscriber(child); + SwitchSubscriber(Subscriber child) { + serializedChild = new SerializedSubscriber(child); + arbiter = new ProducerArbiter(); ssub = new SerialSubscription(); child.add(ssub); child.setProducer(new Producer(){ @Override public void request(long n) { - if (infinite) { - return; - } - if(n == Long.MAX_VALUE) { - infinite = true; - } - InnerSubscriber localSubscriber; - synchronized (guard) { - localSubscriber = currentSubscriber; - if (currentSubscriber == null) { - long r = initialRequested + n; - if (r < 0) { - infinite = true; - } else { - initialRequested = r; - } - } else { - long r = currentSubscriber.requested + n; - if (r < 0) { - infinite = true; - } else { - currentSubscriber.requested = r; - } - } - } - if (localSubscriber != null) { - if (infinite) - localSubscriber.requestMore(Long.MAX_VALUE); - else - localSubscriber.requestMore(n); + if (n > 0) { + arbiter.request(n); } } }); @@ -122,26 +96,18 @@ public void request(long n) { @Override public void onNext(Observable t) { final int id; - long remainingRequest; synchronized (guard) { id = ++index; active = true; - if (infinite) { - remainingRequest = Long.MAX_VALUE; - } else { - remainingRequest = currentSubscriber == null ? initialRequested : currentSubscriber.requested; - } - currentSubscriber = new InnerSubscriber(id, remainingRequest); - currentSubscriber.requested = remainingRequest; + currentSubscriber = new InnerSubscriber(id, arbiter, this); } ssub.set(currentSubscriber); - t.unsafeSubscribe(currentSubscriber); } @Override public void onError(Throwable e) { - s.onError(e); + serializedChild.onError(e); unsubscribe(); } @@ -165,10 +131,10 @@ public void onCompleted() { emitting = true; } drain(localQueue); - s.onCompleted(); + serializedChild.onCompleted(); unsubscribe(); } - void emit(T value, int id, InnerSubscriber innerSubscriber) { + void emit(T value, int id, InnerSubscriber innerSubscriber) { List localQueue; synchronized (guard) { if (id != index) { @@ -178,8 +144,6 @@ void emit(T value, int id, InnerSubscriber innerSubscriber) { if (queue == null) { queue = new ArrayList(); } - if (innerSubscriber.requested != Long.MAX_VALUE) - innerSubscriber.requested--; queue.add(value); return; } @@ -194,11 +158,8 @@ void emit(T value, int id, InnerSubscriber innerSubscriber) { drain(localQueue); if (once) { once = false; - synchronized (guard) { - if (innerSubscriber.requested != Long.MAX_VALUE) - innerSubscriber.requested--; - } - s.onNext(value); + serializedChild.onNext(value); + arbiter.produced(1); } synchronized (guard) { localQueue = queue; @@ -209,7 +170,7 @@ void emit(T value, int id, InnerSubscriber innerSubscriber) { break; } } - } while (!s.isUnsubscribed()); + } while (!serializedChild.isUnsubscribed()); } finally { if (!skipFinal) { synchronized (guard) { @@ -224,16 +185,17 @@ void drain(List localQueue) { } for (Object o : localQueue) { if (nl.isCompleted(o)) { - s.onCompleted(); + serializedChild.onCompleted(); break; } else if (nl.isError(o)) { - s.onError(nl.getError(o)); + serializedChild.onError(nl.getError(o)); break; } else { @SuppressWarnings("unchecked") T t = (T)o; - s.onNext(t); + serializedChild.onNext(t); + arbiter.produced(1); } } } @@ -258,7 +220,7 @@ void error(Throwable e, int id) { } drain(localQueue); - s.onError(e); + serializedChild.onError(e); unsubscribe(); } void complete(int id) { @@ -285,51 +247,45 @@ void complete(int id) { } drain(localQueue); - s.onCompleted(); + serializedChild.onCompleted(); unsubscribe(); } - final class InnerSubscriber extends Subscriber { - - /** - * The number of request that is not acknowledged. - * - * Guarded by guard. - */ - private long requested = 0; - - private final int id; + } + + private static final class InnerSubscriber extends Subscriber { - private final long initialRequested; + private final int id; - public InnerSubscriber(int id, long initialRequested) { - this.id = id; - this.initialRequested = initialRequested; - } + private final ProducerArbiter arbiter; - @Override - public void onStart() { - requestMore(initialRequested); - } + private final SwitchSubscriber parent; - public void requestMore(long n) { - request(n); - } + InnerSubscriber(int id, ProducerArbiter arbiter, SwitchSubscriber parent) { + this.id = id; + this.arbiter = arbiter; + this.parent = parent; + } + + @Override + public void setProducer(Producer p) { + arbiter.setProducer(p); + } - @Override - public void onNext(T t) { - emit(t, id, this); - } + @Override + public void onNext(T t) { + parent.emit(t, id, this); + } - @Override - public void onError(Throwable e) { - error(e, id); - } + @Override + public void onError(Throwable e) { + parent.error(e, id); + } - @Override - public void onCompleted() { - complete(id); - } + @Override + public void onCompleted() { + parent.complete(id); } } + } diff --git a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java index 2534613ab4..332924ba68 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchIfEmptyTest.java @@ -27,7 +27,6 @@ import rx.Observable; import rx.Observable.OnSubscribe; import rx.functions.Action0; -import rx.functions.Action1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; diff --git a/src/test/java/rx/internal/operators/OperatorSwitchTest.java b/src/test/java/rx/internal/operators/OperatorSwitchTest.java index 6b5d3a1f79..63de5d0d81 100644 --- a/src/test/java/rx/internal/operators/OperatorSwitchTest.java +++ b/src/test/java/rx/internal/operators/OperatorSwitchTest.java @@ -25,7 +25,6 @@ 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.CopyOnWriteArrayList; @@ -642,32 +641,34 @@ public Observable call(Long t) { } @Test(timeout = 10000) - public void testSecondaryRequestsAdditivelyAreMoreThanLongMaxValueInducesMaxValueRequestFromUpstream() throws InterruptedException { + public void testSecondaryRequestsAdditivelyAreMoreThanLongMaxValueInducesMaxValueRequestFromUpstream() + throws InterruptedException { final List requests = new CopyOnWriteArrayList(); final Action1 addRequest = new Action1() { @Override public void call(Long n) { requests.add(n); - }}; - TestSubscriber ts = new TestSubscriber(0); + } + }; + TestSubscriber ts = new TestSubscriber(1); Observable.switchOnNext( Observable.interval(100, TimeUnit.MILLISECONDS) .map(new Func1>() { @Override public Observable call(Long t) { - return Observable.from(Arrays.asList(1L, 2L, 3L)).doOnRequest(addRequest); + return Observable.from(Arrays.asList(1L, 2L, 3L)).doOnRequest( + addRequest); } }).take(3)).subscribe(ts); - ts.requestMore(1); - //we will miss two of the first observable + // we will miss two of the first observables Thread.sleep(250); ts.requestMore(Long.MAX_VALUE - 1); ts.requestMore(Long.MAX_VALUE - 1); ts.awaitTerminalEvent(); assertTrue(ts.getOnNextEvents().size() > 0); assertEquals(5, (int) requests.size()); - assertEquals(Long.MAX_VALUE, (long) requests.get(3)); - assertEquals(Long.MAX_VALUE, (long) requests.get(4)); + assertEquals(Long.MAX_VALUE, (long) requests.get(requests.size()-1)); } + } From 0420193e7c075c4fa7986fe03e96fe4a92952cde Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 14 Jul 2015 19:18:04 +0200 Subject: [PATCH 326/857] Merge fully rewritten and other related optimizations --- src/main/java/rx/Observable.java | 74 +- .../rx/internal/operators/OperatorAll.java | 2 +- .../operators/OperatorMapNotification.java | 177 ++- .../rx/internal/operators/OperatorMerge.java | 1277 +++++++++-------- .../operators/OperatorMergeMaxConcurrent.java | 350 ----- .../internal/operators/OperatorPublish.java | 2 +- .../util/ScalarSynchronousObservable.java | 33 +- .../rx/operators/OperatorFlatMapPerf.java | 2 +- .../operators/OperatorFlatMapTest.java | 128 ++ .../OperatorMergeDelayErrorTest.java | 61 +- .../OperatorMergeMaxConcurrentTest.java | 2 +- .../internal/operators/OperatorMergeTest.java | 143 +- 12 files changed, 1211 insertions(+), 1040 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c576ea59f9..d3f21929b3 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1693,7 +1693,11 @@ public final static Observable merge(IterableReactiveX operators documentation: Merge */ + @SuppressWarnings({"unchecked", "rawtypes"}) public final static Observable merge(Observable> source) { + if (source.getClass() == ScalarSynchronousObservable.class) { + return ((ScalarSynchronousObservable)source).scalarFlatMap((Func1)UtilityFunctions.identity()); + } return source.lift(OperatorMerge.instance(false)); } @@ -1721,8 +1725,13 @@ public final static Observable merge(ObservableReactiveX operators documentation: Merge */ + @Experimental + @SuppressWarnings({"unchecked", "rawtypes"}) public final static Observable merge(Observable> source, int maxConcurrent) { - return source.lift(new OperatorMergeMaxConcurrent(maxConcurrent)); + if (source.getClass() == ScalarSynchronousObservable.class) { + return ((ScalarSynchronousObservable)source).scalarFlatMap((Func1)UtilityFunctions.identity()); + } + return source.lift(OperatorMerge.instance(false, maxConcurrent)); } /** @@ -1993,7 +2002,31 @@ public final static Observable merge(Observable t1, Observab public final static Observable merge(Observable[] sequences) { return merge(from(sequences)); } - + + /** + * Flattens an Array of Observables into one Observable, without any transformation, while limiting the + * number of concurrent subscriptions to these Observables. + *

+ * + *

+ * You can combine items emitted by multiple Observables so that they appear as a single Observable, by + * using the {@code merge} method. + *

+ *
Scheduler:
+ *
{@code merge} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param sequences + * the Array of Observables + * @param maxConcurrent + * the maximum number of Observables that may be subscribed to concurrently + * @return an Observable that emits all of the items emitted by the Observables in the Array + * @see ReactiveX operators documentation: Merge + */ + @Experimental + public final static Observable merge(Observable[] sequences, int maxConcurrent) { + return merge(from(sequences), maxConcurrent); + } /** * Flattens an Observable that emits Observables into one Observable, in a way that allows an Observer to * receive all successfully emitted items from all of the source Observables without being interrupted by @@ -2021,6 +2054,37 @@ public final static Observable merge(Observable[] sequences) public final static Observable mergeDelayError(Observable> source) { return source.lift(OperatorMerge.instance(true)); } + /** + * Flattens an Observable that emits Observables into one Observable, in a way that allows an Observer to + * receive all successfully emitted items from all of the source Observables without being interrupted by + * an error notification from one of them, while limiting the + * number of concurrent subscriptions to these Observables. + *

+ * This behaves like {@link #merge(Observable)} except that if any of the merged Observables notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged Observables have finished emitting items. + *

+ * + *

+ * Even if multiple merged Observables send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its Observers once. + *

+ *
Scheduler:
+ *
{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param source + * an Observable that emits Observables + * @param maxConcurrent + * the maximum number of Observables that may be subscribed to concurrently + * @return an Observable that emits all of the items emitted by the Observables emitted by the + * {@code source} Observable + * @see ReactiveX operators documentation: Merge + */ + @Experimental + public final static Observable mergeDelayError(Observable> source, int maxConcurrent) { + return source.lift(OperatorMerge.instance(true, maxConcurrent)); + } /** * Flattens two Observables into one Observable, in a way that allows an Observer to receive all @@ -4618,6 +4682,9 @@ public final Observable firstOrDefault(T defaultValue, Func1ReactiveX operators documentation: FlatMap */ public final Observable flatMap(Func1> func) { + if (getClass() == ScalarSynchronousObservable.class) { + return ((ScalarSynchronousObservable)this).scalarFlatMap(func); + } return merge(map(func)); } @@ -4646,6 +4713,9 @@ public final Observable flatMap(Func1 Observable flatMap(Func1> func, int maxConcurrent) { + if (getClass() == ScalarSynchronousObservable.class) { + return ((ScalarSynchronousObservable)this).scalarFlatMap(func); + } return merge(map(func), maxConcurrent); } diff --git a/src/main/java/rx/internal/operators/OperatorAll.java b/src/main/java/rx/internal/operators/OperatorAll.java index 3f78eeff88..96f0429d01 100644 --- a/src/main/java/rx/internal/operators/OperatorAll.java +++ b/src/main/java/rx/internal/operators/OperatorAll.java @@ -77,4 +77,4 @@ public void onCompleted() { child.setProducer(producer); return s; } -} +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/OperatorMapNotification.java b/src/main/java/rx/internal/operators/OperatorMapNotification.java index 4a2fd03a36..be363663fb 100644 --- a/src/main/java/rx/internal/operators/OperatorMapNotification.java +++ b/src/main/java/rx/internal/operators/OperatorMapNotification.java @@ -15,11 +15,20 @@ */ package rx.internal.operators; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; + import rx.Observable.Operator; +import rx.Producer; import rx.Subscriber; +import rx.Subscription; +import rx.exceptions.MissingBackpressureException; import rx.exceptions.OnErrorThrowable; import rx.functions.Func0; import rx.functions.Func1; +import rx.internal.util.unsafe.SpscArrayQueue; +import rx.internal.util.unsafe.UnsafeAccess; /** * Applies a function of your choosing to every item emitted by an {@code Observable}, and emits the results of @@ -41,13 +50,18 @@ public OperatorMapNotification(Func1 onNext, Func1 call(final Subscriber o) { - return new Subscriber(o) { - + Subscriber subscriber = new Subscriber() { + SingleEmitter emitter; + @Override + public void setProducer(Producer producer) { + emitter = new SingleEmitter(o, producer, this); + o.setProducer(emitter); + } + @Override public void onCompleted() { try { - o.onNext(onCompleted.call()); - o.onCompleted(); + emitter.offerAndComplete(onCompleted.call()); } catch (Throwable e) { o.onError(e); } @@ -56,8 +70,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { try { - o.onNext(onError.call(e)); - o.onCompleted(); + emitter.offerAndComplete(onError.call(e)); } catch (Throwable e2) { o.onError(e); } @@ -66,13 +79,159 @@ public void onError(Throwable e) { @Override public void onNext(T t) { try { - o.onNext(onNext.call(t)); + emitter.offer(onNext.call(t)); } catch (Throwable e) { o.onError(OnErrorThrowable.addValueAsLastCause(e, t)); } } }; + o.add(subscriber); + return subscriber; } - -} + static final class SingleEmitter extends AtomicLong implements Producer, Subscription { + /** */ + private static final long serialVersionUID = -249869671366010660L; + final NotificationLite nl; + final Subscriber child; + final Producer producer; + final Subscription cancel; + final Queue queue; + volatile boolean complete; + /** Guarded by this. */ + boolean emitting; + /** Guarded by this. */ + boolean missed; + + public SingleEmitter(Subscriber child, Producer producer, Subscription cancel) { + this.child = child; + this.producer = producer; + this.cancel = cancel; + this.queue = UnsafeAccess.isUnsafeAvailable() + ? new SpscArrayQueue(2) + : new ConcurrentLinkedQueue(); + + this.nl = NotificationLite.instance(); + } + @Override + public void request(long n) { + for (;;) { + long r = get(); + if (r < 0) { + return; + } + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + if (compareAndSet(r, u)) { + producer.request(n); + drain(); + return; + } + } + } + + void produced(long n) { + for (;;) { + long r = get(); + if (r < 0) { + return; + } + long u = r - n; + if (u < 0) { + throw new IllegalStateException("More produced (" + n + ") than requested (" + r + ")"); + } + if (compareAndSet(r, u)) { + return; + } + } + } + + public void offer(T value) { + if (!queue.offer(value)) { + child.onError(new MissingBackpressureException()); + unsubscribe(); + } else { + drain(); + } + } + public void offerAndComplete(T value) { + if (!this.queue.offer(value)) { + child.onError(new MissingBackpressureException()); + unsubscribe(); + } else { + this.complete = true; + drain(); + } + } + + void drain() { + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + missed = false; + } + boolean skipFinal = false; + try { + for (;;) { + + long r = get(); + boolean c = complete; + boolean empty = queue.isEmpty(); + + if (c && empty) { + child.onCompleted(); + skipFinal = true; + return; + } else + if (r > 0) { + Object v = queue.poll(); + if (v != null) { + child.onNext(nl.getValue(v)); + produced(1); + } else + if (c) { + child.onCompleted(); + skipFinal = true; + return; + } + } + + synchronized (this) { + if (!missed) { + skipFinal = true; + emitting = false; + return; + } + missed = false; + } + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + + @Override + public boolean isUnsubscribed() { + return get() < 0; + } + @Override + public void unsubscribe() { + long r = get(); + if (r != Long.MIN_VALUE) { + r = getAndSet(Long.MIN_VALUE); + if (r != Long.MIN_VALUE) { + cancel.unsubscribe(); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 2da1844ca9..98cb548391 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -15,15 +15,16 @@ */ package rx.internal.operators; -import java.util.Queue; +import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.*; import rx.*; import rx.Observable.Operator; +import rx.Observable; import rx.exceptions.*; -import rx.functions.Func1; import rx.internal.util.*; +import rx.subscriptions.CompositeSubscription; /** * Flattens a list of {@link Observable}s into one {@code Observable}, without any transformation. @@ -49,16 +50,16 @@ * @param * the type of the items emitted by both the source and merged {@code Observable}s */ -public class OperatorMerge implements Operator> { +public final class OperatorMerge implements Operator> { /** Lazy initialization via inner-class holder. */ private static final class HolderNoDelay { /** A singleton instance. */ - static final OperatorMerge INSTANCE = new OperatorMerge(false); + static final OperatorMerge INSTANCE = new OperatorMerge(false, Integer.MAX_VALUE); } /** Lazy initialization via inner-class holder. */ private static final class HolderDelayErrors { /** A singleton instance. */ - static final OperatorMerge INSTANCE = new OperatorMerge(true); + static final OperatorMerge INSTANCE = new OperatorMerge(true, Integer.MAX_VALUE); } /** * @param delayErrors should the merge delay errors? @@ -71,716 +72,754 @@ public static OperatorMerge instance(boolean delayErrors) { } return (OperatorMerge)HolderNoDelay.INSTANCE; } - /* - * benjchristensen => This class is complex and I'm not a fan of it despite writing it. I want to give some background - * as to why for anyone who wants to try and help improve it. - * - * One of my first implementations that added backpressure support (Producer.request) was fairly elegant and used a simple - * queue draining approach. It was simple to understand as all onNext were added to their queues, then a single winner - * would drain the queues, similar to observeOn. It killed the Netflix API when I canaried it. There were two problems: - * (1) performance and (2) object allocation overhead causing massive GC pressure. Remember that merge is one of the most - * used operators (mostly due to flatmap) and is therefore critical to and a limiter of performance in any application. - * - * All subsequent work on this class and the various fast-paths and branches within it have been to achieve the needed functionality - * while reducing or eliminating object allocation and keeping performance acceptable. - * - * This has meant adopting strategies such as: - * - * - ring buffers instead of growable queues - * - object pooling - * - skipping request logic when downstream does not need backpressure - * - ScalarValueQueue for optimizing synchronous single-value Observables - * - adopting data structures that use Unsafe (and gating them based on environment so non-Oracle JVMs still work) - * - * It has definitely increased the complexity and maintenance cost of this class, but the performance gains have been significant. - * - * The biggest cost of the increased complexity is concurrency bugs and reasoning through what's going on. - * - * I'd love to have contributions that improve this class, but keep in mind the performance and GC pressure. - * The benchmarks I use are in the JMH OperatorMergePerf class. GC memory pressure is tested using Java Flight Recorder - * to track object allocation. + /** + * Creates a new instance of the operator with the given delayError and maxConcurrency settings. + * @param delayErrors + * @param maxConcurrent the maximum number of concurrent subscriptions or Integer.MAX_VALUE for unlimited + * @return */ - - private OperatorMerge() { - this.delayErrors = false; + public static OperatorMerge instance(boolean delayErrors, int maxConcurrent) { + if (maxConcurrent == Integer.MAX_VALUE) { + return instance(delayErrors); + } + return new OperatorMerge(delayErrors, maxConcurrent); } - private OperatorMerge(boolean delayErrors) { + final boolean delayErrors; + final int maxConcurrent; + + private OperatorMerge(boolean delayErrors, int maxConcurrent) { this.delayErrors = delayErrors; + this.maxConcurrent = maxConcurrent; } - private final boolean delayErrors; - @Override public Subscriber> call(final Subscriber child) { - return new MergeSubscriber(child, delayErrors); - + MergeSubscriber subscriber = new MergeSubscriber(child, delayErrors, maxConcurrent); + MergeProducer producer = new MergeProducer(subscriber); + subscriber.producer = producer; + + child.add(subscriber); + child.setProducer(producer); + + return subscriber; } - private static final class MergeSubscriber extends Subscriber> { - final NotificationLite on = NotificationLite.instance(); - final Subscriber actual; - private final MergeProducer mergeProducer; - private int wip; - private boolean completed; - private final boolean delayErrors; - private ConcurrentLinkedQueue exceptions; - - private volatile SubscriptionIndexedRingBuffer> childrenSubscribers; - - private volatile RxRingBuffer scalarValueQueue = null; - - /* protected by lock on MergeSubscriber instance */ - private int missedEmitting = 0; - private boolean emitLock = false; - - /** - * Using synchronized(this) for `emitLock` instead of ReentrantLock or AtomicInteger is faster when there is no contention. - * - *
 {@code
-         * Using ReentrantLock:
-         * r.o.OperatorMergePerf.merge1SyncStreamOfN           1000  thrpt         5    44185.294     1295.565    ops/s
-         * 
-         * Using synchronized(this):
-         * r.o.OperatorMergePerf.merge1SyncStreamOfN           1000  thrpt         5    79715.981     3704.486    ops/s
-         * 
-         * Still slower though than allowing concurrency:
-         * r.o.OperatorMergePerf.merge1SyncStreamOfN           1000  thrpt         5   149331.046     4851.290    ops/s
-         * } 
- */ - - public MergeSubscriber(Subscriber actual, boolean delayErrors) { - super(actual); - this.actual = actual; - this.mergeProducer = new MergeProducer(this); - this.delayErrors = delayErrors; - // decoupled the subscription chain because we need to decouple and control backpressure - actual.add(this); - actual.setProducer(mergeProducer); - } + static final class MergeProducer extends AtomicLong implements Producer { + /** */ + private static final long serialVersionUID = -1214379189873595503L; - @Override - public void onStart() { - // we request backpressure so we can handle long-running Observables that are enqueueing, such as flatMap use cases - // we decouple the Producer chain while keeping the Subscription chain together (perf benefit) via super(actual) - request(RxRingBuffer.SIZE); + final MergeSubscriber subscriber; + + public MergeProducer(MergeSubscriber subscriber) { + this.subscriber = subscriber; } - - /* - * This is expected to be executed sequentially as per the Rx contract or it will not work. - */ + @Override - public void onNext(Observable t) { - if (t instanceof ScalarSynchronousObservable) { - ScalarSynchronousObservable t2 = (ScalarSynchronousObservable)t; - handleScalarSynchronousObservable(t2); - } else { - if (t == null || isUnsubscribed()) { + public void request(long n) { + if (n > 0) { + if (get() == Long.MAX_VALUE) { return; } + BackpressureUtils.getAndAddRequest(this, n); + subscriber.emit(); + } else + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required"); + } + } + public long produced(int n) { + return addAndGet(-n); + } + } + + /** + * The subscriber that observes Observables. + * @param the value type + */ + static final class MergeSubscriber extends Subscriber> { + final Subscriber child; + final boolean delayErrors; + final int maxConcurrent; + + MergeProducer producer; + + volatile RxRingBuffer queue; + + /** Tracks the active subscriptions to sources. */ + volatile CompositeSubscription subscriptions; + /** Due to the emission loop, we need to store errors somewhere if !delayErrors. */ + volatile ConcurrentLinkedQueue errors; + + final NotificationLite nl; + + volatile boolean done; + + /** Guarded by this. */ + boolean emitting; + /** Guarded by this. */ + boolean missed; + + final Object innerGuard; + /** Copy-on-write array, guarded by innerGuard. */ + volatile InnerSubscriber[] innerSubscribers; + + /** Used to generate unique InnerSubscriber IDs. Modified from onNext only. */ + long uniqueId; + + /** Which was the last InnerSubscriber that emitted? Accessed if emitting == true. */ + long lastId; + /** What was its index in the innerSubscribers array? Accessed if emitting == true. */ + int lastIndex; + + /** An empty array to avoid creating new empty arrays in removeInner. */ + static final InnerSubscriber[] EMPTY = new InnerSubscriber[0]; + + public MergeSubscriber(Subscriber child, boolean delayErrors, int maxConcurrent) { + this.child = child; + this.delayErrors = delayErrors; + this.maxConcurrent = maxConcurrent; + this.nl = NotificationLite.instance(); + this.innerGuard = new Object(); + this.innerSubscribers = EMPTY; + long r = Math.min(maxConcurrent, RxRingBuffer.SIZE); + request(r); + } + + Queue getOrCreateErrorQueue() { + ConcurrentLinkedQueue q = errors; + if (q == null) { synchronized (this) { - // synchronized here because `wip` can be concurrently changed by children Observables - wip++; + q = errors; + if (q == null) { + q = new ConcurrentLinkedQueue(); + errors = q; + } } - handleNewSource(t); } + return q; } - - private void handleNewSource(Observable t) { - if (childrenSubscribers == null) { - // lazily create this only if we receive Observables we need to subscribe to - childrenSubscribers = new SubscriptionIndexedRingBuffer>(); - add(childrenSubscribers); + CompositeSubscription getOrCreateComposite() { + CompositeSubscription c = subscriptions; + if (c == null) { + boolean shouldAdd = false; + synchronized (this) { + c = subscriptions; + if (c == null) { + c = new CompositeSubscription(); + subscriptions = c; + shouldAdd = true; + } + } + if (shouldAdd) { + add(c); + } } - MergeProducer producerIfNeeded = null; - // if we have received a request then we need to respect it, otherwise we fast-path - if (mergeProducer.requested != Long.MAX_VALUE) { - /** - *
 {@code
-                 * With this optimization:
-                 * 
-                 * r.o.OperatorMergePerf.merge1SyncStreamOfN      1000  thrpt         5    57100.080     4686.331    ops/s
-                 * r.o.OperatorMergePerf.merge1SyncStreamOfN   1000000  thrpt         5       60.875        1.622    ops/s
-                 *  
-                 * Without this optimization:
-                 * 
-                 * r.o.OperatorMergePerf.merge1SyncStreamOfN      1000  thrpt         5    29863.945     1858.002    ops/s
-                 * r.o.OperatorMergePerf.merge1SyncStreamOfN   1000000  thrpt         5       30.516        1.087    ops/s
-                 * } 
- */ - producerIfNeeded = mergeProducer; + return c; + } + + @Override + public void onNext(Observable t) { + if (t == null) { + return; } - InnerSubscriber i = new InnerSubscriber(this, producerIfNeeded); - i.sindex = childrenSubscribers.add(i); - t.unsafeSubscribe(i); - if (!isUnsubscribed()) { - request(1); + if (t instanceof ScalarSynchronousObservable) { + tryEmit(((ScalarSynchronousObservable)t).get()); + } else { + InnerSubscriber inner = new InnerSubscriber(this, uniqueId++); + addInner(inner); + t.unsafeSubscribe(inner); + emit(); } } - - private void handleScalarSynchronousObservable(ScalarSynchronousObservable t) { - // fast-path for scalar, synchronous values such as Observable.from(int) - /** - * Without this optimization: - * - *
 {@code
-             * Benchmark                                          (size)   Mode   Samples        Score  Score error    Units
-             * r.o.OperatorMergePerf.oneStreamOfNthatMergesIn1         1  thrpt         5  2,418,452.409   130572.665    ops/s
-             * r.o.OperatorMergePerf.oneStreamOfNthatMergesIn1      1000  thrpt         5     5,690.456       94.958    ops/s
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN         1000000  thrpt         5          takes too long
-             * 
-             * With this optimization:
-             * 
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN               1  thrpt         5  5,475,300.198   156741.334    ops/s
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN            1000  thrpt         5    68,932.278     1311.023    ops/s
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN         1000000  thrpt         5       64.405        0.611    ops/s
-             * } 
- * - */ - if (mergeProducer.requested == Long.MAX_VALUE) { - handleScalarSynchronousObservableWithoutRequestLimits(t); + + private void reportError() { + List list = new ArrayList(errors); + if (list.size() == 1) { + child.onError(list.get(0)); } else { - handleScalarSynchronousObservableWithRequestLimits(t); + child.onError(new CompositeException(list)); } } - - private void handleScalarSynchronousObservableWithoutRequestLimits(ScalarSynchronousObservable t) { - T value = t.get(); - if (getEmitLock()) { - boolean moreToDrain; - try { - actual.onNext(value); - } finally { - moreToDrain = releaseEmitLock(); - } - if (moreToDrain) { - drainQueuesIfNeeded(); - } - request(1); - return; - } else { - try { - getOrCreateScalarValueQueue().onNext(value); - } catch (MissingBackpressureException e) { - onError(e); - } - return; + + @Override + public void onError(Throwable e) { + getOrCreateErrorQueue().offer(e); + done = true; + emit(); + } + @Override + public void onCompleted() { + done = true; + emit(); + } + + void addInner(InnerSubscriber inner) { + getOrCreateComposite().add(inner); + synchronized (innerGuard) { + InnerSubscriber[] a = innerSubscribers; + int n = a.length; + InnerSubscriber[] b = new InnerSubscriber[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + innerSubscribers = b; } } - - private void handleScalarSynchronousObservableWithRequestLimits(ScalarSynchronousObservable t) { - if (getEmitLock()) { - boolean emitted = false; - boolean moreToDrain; - boolean isReturn = false; - try { - long r = mergeProducer.requested; - if (r > 0) { - emitted = true; - actual.onNext(t.get()); - MergeProducer.REQUESTED.decrementAndGet(mergeProducer); - // we handle this Observable without ever incrementing the wip or touching other machinery so just return here - isReturn = true; + void removeInner(InnerSubscriber inner) { + RxRingBuffer q = inner.queue; + if (q != null) { + q.release(); + } + // subscription is non-null here because the very first addInner will create it before + // this can be called + subscriptions.remove(inner); + synchronized (innerGuard) { + InnerSubscriber[] a = innerSubscribers; + int n = a.length; + int j = -1; + // locate the inner + for (int i = 0; i < n; i++) { + if (inner.equals(a[i])) { + j = i; + break; } - } finally { - moreToDrain = releaseEmitLock(); } - if (moreToDrain) { - drainQueuesIfNeeded(); - } - if (emitted) { - request(1); + if (j < 0) { + return; } - if (isReturn) { + if (n == 1) { + innerSubscribers = EMPTY; return; } - } - - // if we didn't return above we need to enqueue - // enqueue the values for later delivery - try { - getOrCreateScalarValueQueue().onNext(t.get()); - } catch (MissingBackpressureException e) { - onError(e); + InnerSubscriber[] b = new InnerSubscriber[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + innerSubscribers = b; } } - - private RxRingBuffer getOrCreateScalarValueQueue() { - RxRingBuffer svq = scalarValueQueue; - if (svq == null) { - svq = RxRingBuffer.getSpscInstance(); - scalarValueQueue = svq; + + /** + * Tries to emit the value directly to the child if + * no concurrent emission is happening at the moment. + *

+ * Since the scalar-value queue optimization applies + * to both the main source and the inner subscribers, + * we handle things in a shared manner. + * + * @param subscriber + * @param value + */ + void tryEmit(InnerSubscriber subscriber, T value) { + boolean success = false; + long r = producer.get(); + if (r != 0L) { + synchronized (this) { + // if nobody is emitting and child has available requests + if (!emitting) { + emitting = true; + success = true; + } + } } - return svq; - } - - private synchronized boolean releaseEmitLock() { - emitLock = false; - if (missedEmitting == 0) { - return false; + if (success) { + emitScalar(subscriber, value, r); } else { - return true; + queueScalar(subscriber, value); } } - private synchronized boolean getEmitLock() { - if (emitLock) { - missedEmitting++; - return false; - } else { - emitLock = true; - missedEmitting = 0; - return true; + protected void queueScalar(InnerSubscriber subscriber, T value) { + /* + * If the attempt to make a fast-path emission failed + * due to lack of requests or an ongoing emission, + * enqueue the value and try the slow emission path. + */ + RxRingBuffer q = subscriber.queue; + if (q == null) { + q = RxRingBuffer.getSpscInstance(); + subscriber.add(q); + subscriber.queue = q; + } + try { + q.onNext(nl.next(value)); + } catch (MissingBackpressureException ex) { + subscriber.unsubscribe(); + subscriber.onError(ex); + return; + } catch (IllegalStateException ex) { + if (!subscriber.isUnsubscribed()) { + subscriber.unsubscribe(); + subscriber.onError(ex); + } + return; } + emit(); } - private boolean drainQueuesIfNeeded() { - while (true) { - if (getEmitLock()) { - int emitted = 0; - boolean moreToDrain; - try { - emitted = drainScalarValueQueue(); - drainChildrenQueues(); - } finally { - moreToDrain = releaseEmitLock(); + protected void emitScalar(InnerSubscriber subscriber, T value, long r) { + boolean skipFinal = false; + try { + try { + child.onNext(value); + } catch (Throwable t) { + if (!delayErrors) { + Exceptions.throwIfFatal(t); + skipFinal = true; + subscriber.unsubscribe(); + subscriber.onError(t); + return; } - // request outside of lock - if (emitted > 0) { - request(emitted); + getOrCreateErrorQueue().offer(t); + } + if (r != Long.MAX_VALUE) { + producer.produced(1); + } + subscriber.requestMore(1); + // check if some state changed while emitting + synchronized (this) { + skipFinal = true; + if (!missed) { + emitting = false; + return; } - if (!moreToDrain) { - return true; + missed = false; + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; } - // otherwise we'll loop and get whatever was added - } else { - return false; } } + /* + * In the synchronized block below request(1) we check + * if there was a concurrent emission attempt and if there was, + * we stay in emission mode and enter the emission loop + * which will take care all the queued up state and + * emission possibilities. + */ + emitLoop(); } - int lastDrainedIndex = 0; - - /** - * ONLY call when holding the EmitLock. - */ - private void drainChildrenQueues() { - if (childrenSubscribers != null) { - lastDrainedIndex = childrenSubscribers.forEach(DRAIN_ACTION, lastDrainedIndex); - } + public void requestMore(long n) { + request(n); } - + /** - * ONLY call when holding the EmitLock. + * Tries to emit the value directly to the child if + * no concurrent emission is happening at the moment. + *

+ * Since the scalar-value queue optimization applies + * to both the main source and the inner subscribers, + * we handle things in a shared manner. + * + * @param subscriber + * @param value */ - private int drainScalarValueQueue() { - RxRingBuffer svq = scalarValueQueue; - if (svq != null) { - long r = mergeProducer.requested; - int emittedWhileDraining = 0; - if (r < 0) { - // drain it all - Object o = null; - while ((o = svq.poll()) != null) { - on.accept(actual, o); - emittedWhileDraining++; - } - } else if (r > 0) { - // drain what was requested - long toEmit = r; - for (int i = 0; i < toEmit; i++) { - Object o = svq.poll(); - if (o == null) { - break; - } else { - on.accept(actual, o); - emittedWhileDraining++; - } + void tryEmit(T value) { + boolean success = false; + long r = producer.get(); + if (r != 0L) { + synchronized (this) { + // if nobody is emitting and child has available requests + if (!emitting) { + emitting = true; + success = true; } - // decrement the number we emitted from outstanding requests - MergeProducer.REQUESTED.getAndAdd(mergeProducer, -emittedWhileDraining); } - return emittedWhileDraining; } - return 0; + if (success) { + emitScalar(value, r); + } else { + queueScalar(value); + } } - final Func1, Boolean> DRAIN_ACTION = new Func1, Boolean>() { - - @Override - public Boolean call(InnerSubscriber s) { - if (s.q != null) { - long r = mergeProducer.requested; - int emitted = s.drainQueue(); - if (emitted > 0) { - s.requestMore(emitted); - } - if (emitted == r) { - // we emitted as many as were requested so stop the forEach loop - return Boolean.FALSE; - } - } - return Boolean.TRUE; + protected void queueScalar(T value) { + /* + * If the attempt to make a fast-path emission failed + * due to lack of requests or an ongoing emission, + * enqueue the value and try the slow emission path. + */ + RxRingBuffer q = this.queue; + if (q == null) { + q = RxRingBuffer.getSpscInstance(); + this.add(q); + this.queue = q; } - - }; - - @Override - public void onError(Throwable e) { - if (!completed) { - completed = true; - innerError(e, true); + try { + q.onNext(nl.next(value)); + } catch (MissingBackpressureException ex) { + this.unsubscribe(); + this.onError(ex); + return; + } catch (IllegalStateException ex) { + if (!this.isUnsubscribed()) { + this.unsubscribe(); + this.onError(ex); + } + return; } + emit(); } - - private void innerError(Throwable e, boolean parent) { - if (delayErrors) { - synchronized (this) { - if (exceptions == null) { - exceptions = new ConcurrentLinkedQueue(); + + protected void emitScalar(T value, long r) { + boolean skipFinal = false; + try { + try { + child.onNext(value); + } catch (Throwable t) { + if (!delayErrors) { + Exceptions.throwIfFatal(t); + skipFinal = true; + this.unsubscribe(); + this.onError(t); + return; } + getOrCreateErrorQueue().offer(t); } - exceptions.add(e); - boolean sendOnComplete = false; + if (r != Long.MAX_VALUE) { + producer.produced(1); + } + this.requestMore(1); + // check if some state changed while emitting synchronized (this) { - if (!parent) { - wip--; - } - if ((wip == 0 && completed) || (wip < 0)) { - sendOnComplete = true; + skipFinal = true; + if (!missed) { + emitting = false; + return; } + missed = false; } - if (sendOnComplete) { - drainAndComplete(); - } - } else { - actual.onError(e); - } - } - - @Override - public void onCompleted() { - boolean c = false; - synchronized (this) { - completed = true; - if (wip == 0) { - c = true; + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } } } - if (c) { - // complete outside of lock - drainAndComplete(); - } + /* + * In the synchronized block below request(1) we check + * if there was a concurrent emission attempt and if there was, + * we stay in emission mode and enter the emission loop + * which will take care all the queued up state and + * emission possibilities. + */ + emitLoop(); } - - void completeInner(InnerSubscriber s) { - boolean sendOnComplete = false; + + void emit() { synchronized (this) { - wip--; - if (wip == 0 && completed) { - sendOnComplete = true; + if (emitting) { + missed = true; + return; } + emitting = true; } - childrenSubscribers.remove(s.sindex); - if (sendOnComplete) { - drainAndComplete(); - } + emitLoop(); } - - private void drainAndComplete() { - boolean moreToDrain = true; - while (moreToDrain) { - synchronized (this) { - missedEmitting = 0; - } - drainScalarValueQueue(); - drainChildrenQueues(); - synchronized (this) { - moreToDrain = missedEmitting > 0; - } - } - RxRingBuffer svq = scalarValueQueue; - if (svq == null || svq.isEmpty()) { - if (delayErrors) { - Queue es = null; - synchronized (this) { - es = exceptions; + /** + * The standard emission loop serializing events and requests. + */ + void emitLoop() { + boolean skipFinal = false; + try { + final Subscriber child = this.child; + for (;;) { + // eagerly check if child unsubscribed or we reached a terminal state. + if (checkTerminate()) { + skipFinal = true; + return; } - if (es != null) { - if (es.isEmpty()) { - actual.onCompleted(); - } else if (es.size() == 1) { - actual.onError(es.poll()); + RxRingBuffer svq = queue; + + long r = producer.get(); + boolean unbounded = r == Long.MAX_VALUE; + + // count the number of 'completed' sources to replenish them in batches + int replenishMain = 0; + + // try emitting as many scalars as possible + if (svq != null) { + for (;;) { + int scalarEmission = 0; + Object o = null; + while (r > 0) { + o = svq.poll(); + // eagerly check if child unsubscribed or we reached a terminal state. + if (checkTerminate()) { + skipFinal = true; + return; + } + if (o == null) { + break; + } + T v = nl.getValue(o); + // if child throws, report bounce it back immediately + try { + child.onNext(v); + } catch (Throwable t) { + if (!delayErrors) { + Exceptions.throwIfFatal(t); + skipFinal = true; + unsubscribe(); + child.onError(t); + return; + } + getOrCreateErrorQueue().offer(t); + } + replenishMain++; + scalarEmission++; + r--; + } + if (scalarEmission > 0) { + if (unbounded) { + r = Long.MAX_VALUE; + } else { + r = producer.produced(scalarEmission); + } + } + if (r == 0L || o == null) { + break; + } + } + } + + /* + * We need to read done before innerSubscribers because innerSubcribers 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. + */ + boolean d = done; + // re-read svq because it could have been created + // asynchronously just before done was set to true. + svq = queue; + // read the current set of inner subscribers + InnerSubscriber[] inner = innerSubscribers; + int n = inner.length; + + // check if upstream is done, there are no scalar values + // and no active inner subscriptions + if (d && (svq == null || svq.isEmpty()) && n == 0) { + Queue e = errors; + if (e == null || e.isEmpty()) { + child.onCompleted(); } else { - actual.onError(new CompositeException(es)); + reportError(); + } + if (svq != null) { + svq.release(); } - } else { - actual.onCompleted(); + skipFinal = true; + return; + } + + boolean innerCompleted = false; + if (n > 0) { + // let's continue the round-robin emission from last location + long startId = lastId; + int index = lastIndex; + + // in case there were changes in the array or the index + // no longer points to the inner with the cached id + if (n <= index || inner[index].id != startId) { + if (n <= index) { + index = 0; + } + // try locating the inner with the cached index + int j = index; + for (int i = 0; i < n; i++) { + if (inner[j].id == startId) { + break; + } + // wrap around in round-robin fashion + j++; + if (j == n) { + j = 0; + } + } + // if we found it again, j will point to it + // otherwise, we continue with the replacement at j + index = j; + lastIndex = j; + lastId = inner[j].id; + } + + int j = index; + // loop through all sources once to avoid delaying any new sources too much + for (int i = 0; i < n; i++) { + // eagerly check if child unsubscribed or we reached a terminal state. + if (checkTerminate()) { + skipFinal = true; + return; + } + @SuppressWarnings("unchecked") + InnerSubscriber is = (InnerSubscriber)inner[j]; + + Object o = null; + for (;;) { + int produced = 0; + while (r > 0) { + // eagerly check if child unsubscribed or we reached a terminal state. + if (checkTerminate()) { + skipFinal = true; + return; + } + RxRingBuffer q = is.queue; + if (q == null) { + break; + } + o = q.poll(); + if (o == null) { + break; + } + T v = nl.getValue(o); + // if child throws, report bounce it back immediately + try { + child.onNext(v); + } catch (Throwable t) { + skipFinal = true; + Exceptions.throwIfFatal(t); + try { + child.onError(t); + } finally { + unsubscribe(); + } + return; + } + r--; + produced++; + } + if (produced > 0) { + if (!unbounded) { + r = producer.produced(produced); + } else { + r = Long.MAX_VALUE; + } + is.requestMore(produced); + } + // if we run out of requests or queued values, break + if (r == 0 || o == null) { + break; + } + } + boolean innerDone = is.done; + RxRingBuffer innerQueue = is.queue; + if (innerDone && (innerQueue == null || innerQueue.isEmpty())) { + removeInner(is); + if (checkTerminate()) { + skipFinal = true; + return; + } + replenishMain++; + innerCompleted = true; + } + // if we run out of requests, don't try the other sources + if (r == 0) { + break; + } + + // wrap around in round-robin fashion + j++; + if (j == n) { + j = 0; + } + } + // if we run out of requests or just completed a round, save the index and id + lastIndex = j; + lastId = inner[j].id; + } + + if (replenishMain > 0) { + request(replenishMain); + } + // if one or more inner completed, loop again to see if we can terminate the whole stream + if (innerCompleted) { + continue; + } + // in case there were updates to the state, we loop again + synchronized (this) { + if (!missed) { + skipFinal = true; + emitting = false; + break; + } + missed = false; + } + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; } - } else { - actual.onCompleted(); } } } - - } - - private static final class MergeProducer implements Producer { - - private final MergeSubscriber ms; - - public MergeProducer(MergeSubscriber ms) { - this.ms = ms; - } - - private volatile long requested = 0; - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(MergeProducer.class, "requested"); - - @Override - public void request(long n) { - if (requested == Long.MAX_VALUE) { - return; + + /** + * Check if the operator reached some terminal state: child unsubscribed, + * an error was reported and we don't delay errors. + * @return true if the child unsubscribed or there are errors available and merge doesn't delay errors. + */ + boolean checkTerminate() { + if (child.isUnsubscribed()) { + return true; } - if (n == Long.MAX_VALUE) { - requested = Long.MAX_VALUE; - } else { - BackpressureUtils.getAndAddRequest(REQUESTED, this, n); - if (ms.drainQueuesIfNeeded()) { - boolean sendComplete = false; - synchronized (ms) { - if (ms.wip == 0 && ms.scalarValueQueue != null && ms.scalarValueQueue.isEmpty()) { - sendComplete = true; - } - } - if (sendComplete) { - ms.drainAndComplete(); - } + Queue e = errors; + if (!delayErrors && (e != null && !e.isEmpty())) { + try { + reportError(); + } finally { + unsubscribe(); } + return true; } + return false; } - } - - private static final class InnerSubscriber extends Subscriber { - public int sindex; - final MergeSubscriber parentSubscriber; - final MergeProducer producer; - /** Make sure the inner termination events are delivered only once. */ - @SuppressWarnings("unused") - volatile int terminated; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE_TERMINATED = AtomicIntegerFieldUpdater.newUpdater(InnerSubscriber.class, "terminated"); - - private final RxRingBuffer q = RxRingBuffer.getSpscInstance(); - - public InnerSubscriber(MergeSubscriber parent, MergeProducer producer) { - this.parentSubscriber = parent; - this.producer = producer; - add(q); - request(q.capacity()); + static final class InnerSubscriber extends Subscriber { + final MergeSubscriber parent; + final long id; + volatile boolean done; + volatile RxRingBuffer queue; + int outstanding; + static final int limit = RxRingBuffer.SIZE / 4; + + public InnerSubscriber(MergeSubscriber parent, long id) { + this.parent = parent; + this.id = id; + } + @Override + public void onStart() { + outstanding = RxRingBuffer.SIZE; + request(RxRingBuffer.SIZE); } - @Override public void onNext(T t) { - emit(t, false); + parent.tryEmit(this, t); } - @Override public void onError(Throwable e) { - // it doesn't go through queues, it immediately onErrors and tears everything down - if (ONCE_TERMINATED.compareAndSet(this, 0, 1)) { - parentSubscriber.innerError(e, false); - } + done = true; + parent.getOrCreateErrorQueue().offer(e); + parent.emit(); } - @Override public void onCompleted() { - if (ONCE_TERMINATED.compareAndSet(this, 0, 1)) { - emit(null, true); - } + done = true; + parent.emit(); } - public void requestMore(long n) { - request(n); - } - - private void emit(T t, boolean complete) { - boolean drain = false; - boolean enqueue = true; - /** - * This optimization to skip the queue is messy ... but it makes a big difference in performance when merging a single stream - * with many values, or many intermittent streams without contention. It doesn't make much of a difference if there is contention. - * - * Below are some of the relevant benchmarks to show the difference. - * - *

 {@code
-             * With this fast-path:
-             * 
-             * Benchmark                                          (size)   Mode   Samples        Score  Score error    Units
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN               1  thrpt         5  5344143.680   393484.592    ops/s
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN            1000  thrpt         5    83582.662     4293.755    ops/s +++
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN         1000000  thrpt         5       73.889        4.477    ops/s +++
-             * 
-             * r.o.OperatorMergePerf.mergeNSyncStreamsOfN              1  thrpt         5  5799265.333   199205.296    ops/s +
-             * r.o.OperatorMergePerf.mergeNSyncStreamsOfN           1000  thrpt         5       62.655        2.521    ops/s +++
-             * 
-             * r.o.OperatorMergePerf.mergeTwoAsyncStreamsOfN           1  thrpt         5    76925.616     4909.174    ops/s
-             * r.o.OperatorMergePerf.mergeTwoAsyncStreamsOfN        1000  thrpt         5     3634.977      242.469    ops/s
-             * 
-             * Without:
-             * 
-             * Benchmark                                          (size)   Mode   Samples        Score  Score error    Units
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN               1  thrpt         5  5099295.678   159539.842    ops/s
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN            1000  thrpt         5    18196.671    10053.298    ops/s
-             * r.o.OperatorMergePerf.merge1SyncStreamOfN         1000000  thrpt         5       19.184        1.028    ops/s
-             * 
-             * r.o.OperatorMergePerf.mergeNSyncStreamsOfN              1  thrpt         5  5591612.719   591821.763    ops/s
-             * r.o.OperatorMergePerf.mergeNSyncStreamsOfN           1000  thrpt         5       21.018        3.251    ops/s
-             * 
-             * r.o.OperatorMergePerf.mergeTwoAsyncStreamsOfN           1  thrpt         5    72692.073    18395.031    ops/s
-             * r.o.OperatorMergePerf.mergeTwoAsyncStreamsOfN        1000  thrpt         5     4379.093      386.368    ops/s
-             * } 
- * - * It looks like it may cause a slowdown in highly contended cases (like 'mergeTwoAsyncStreamsOfN' above) as instead of just - * putting in the queue, it attempts to get the lock. We are optimizing for the non-contended case. - */ - if (parentSubscriber.getEmitLock()) { - long emitted = 0; - enqueue = false; - try { - // drain the queue if there is anything in it before emitting the current value - emitted += drainQueue(); - // } - if (producer == null) { - // no backpressure requested - if (complete) { - parentSubscriber.completeInner(this); - } else { - try { - parentSubscriber.actual.onNext(t); - } catch (Throwable e) { - // special error handling due to complexity of merge - onError(OnErrorThrowable.addValueAsLastCause(e, t)); - } - emitted++; - } - } else { - // this needs to check q.count() as draining above may not have drained the full queue - // perf tests show this to be okay, though different queue implementations could perform poorly with this - if (producer.requested > 0 && q.count() == 0) { - if (complete) { - parentSubscriber.completeInner(this); - } else { - try { - parentSubscriber.actual.onNext(t); - } catch (Throwable e) { - // special error handling due to complexity of merge - onError(OnErrorThrowable.addValueAsLastCause(e, t)); - } - emitted++; - MergeProducer.REQUESTED.decrementAndGet(producer); - } - } else { - // no requests available, so enqueue it - enqueue = true; - } - } - } finally { - drain = parentSubscriber.releaseEmitLock(); - } - // request upstream what we just emitted - if(emitted > 0) { - request(emitted); - } - } - if (enqueue) { - enqueue(t, complete); - drain = true; - } - if (drain) { - /** - * This extra check for whether to call drain is ugly, but it helps: - *
 {@code
-                 * Without:
-                 * r.o.OperatorMergePerf.mergeNSyncStreamsOfN     1000  thrpt         5       61.812        1.455    ops/s
-                 * 
-                 * With:
-                 * r.o.OperatorMergePerf.mergeNSyncStreamsOfN     1000  thrpt         5       78.795        1.766    ops/s
-                 * } 
- */ - parentSubscriber.drainQueuesIfNeeded(); - } - } - - private void enqueue(T t, boolean complete) { - try { - if (complete) { - q.onCompleted(); - } else { - q.onNext(t); - } - } catch (MissingBackpressureException e) { - onError(e); - } - } - - private int drainRequested() { - int emitted = 0; - // drain what was requested - long toEmit = producer.requested; - Object o; - for (int i = 0; i < toEmit; i++) { - o = q.poll(); - if (o == null) { - // no more items - break; - } else if (q.isCompleted(o)) { - parentSubscriber.completeInner(this); - } else { - try { - if (!q.accept(o, parentSubscriber.actual)) { - emitted++; - } - } catch (Throwable e) { - // special error handling due to complexity of merge - onError(OnErrorThrowable.addValueAsLastCause(e, o)); - } - } - } - - // decrement the number we emitted from outstanding requests - MergeProducer.REQUESTED.getAndAdd(producer, -emitted); - return emitted; - } - - private int drainAll() { - int emitted = 0; - // drain it all - Object o; - while ((o = q.poll()) != null) { - if (q.isCompleted(o)) { - parentSubscriber.completeInner(this); - } else { - try { - if (!q.accept(o, parentSubscriber.actual)) { - emitted++; - } - } catch (Throwable e) { - // special error handling due to complexity of merge - onError(OnErrorThrowable.addValueAsLastCause(e, o)); - } - } + int r = outstanding - (int)n; + if (r > limit) { + outstanding = r; + return; } - return emitted; - } - - private int drainQueue() { - if (producer != null) { - return drainRequested(); - } else { - return drainAll(); + outstanding = RxRingBuffer.SIZE; + int k = RxRingBuffer.SIZE - r; + if (k > 0) { + request(k); } } - } -} \ No newline at end of file + }} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java b/src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java deleted file mode 100644 index 9f28f3199e..0000000000 --- a/src/main/java/rx/internal/operators/OperatorMergeMaxConcurrent.java +++ /dev/null @@ -1,350 +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.*; -import java.util.concurrent.atomic.*; - -import rx.*; -import rx.Observable.Operator; -import rx.Observable; -import rx.exceptions.MissingBackpressureException; -import rx.internal.util.RxRingBuffer; -import rx.observers.SerializedSubscriber; -import rx.subscriptions.CompositeSubscription; - -/** - * Flattens a list of Observables into one Observable sequence, without any transformation. - *

- * - *

- * You can combine the items emitted by multiple Observables so that they act like a single - * Observable, by using the merge operation. - * - * @param the emitted value type - */ -public final class OperatorMergeMaxConcurrent implements Operator> { - final int maxConcurrency; - - public OperatorMergeMaxConcurrent(int maxConcurrency) { - this.maxConcurrency = maxConcurrency; - } - - @Override - public Subscriber> call(Subscriber child) { - final SerializedSubscriber s = new SerializedSubscriber(child); - final CompositeSubscription csub = new CompositeSubscription(); - child.add(csub); - - SourceSubscriber ssub = new SourceSubscriber(maxConcurrency, s, csub); - child.setProducer(new MergeMaxConcurrentProducer(ssub)); - - return ssub; - } - /** Routes the requests from downstream to the sourcesubscriber. */ - static final class MergeMaxConcurrentProducer implements Producer { - final SourceSubscriber ssub; - public MergeMaxConcurrentProducer(SourceSubscriber ssub) { - this.ssub = ssub; - } - @Override - public void request(long n) { - ssub.downstreamRequest(n); - } - } - static final class SourceSubscriber extends Subscriber> { - final NotificationLite nl = NotificationLite.instance(); - final int maxConcurrency; - final Subscriber s; - final CompositeSubscription csub; - final Object guard; - - volatile int wip; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP - = AtomicIntegerFieldUpdater.newUpdater(SourceSubscriber.class, "wip"); - volatile int sourceIndex; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater SOURCE_INDEX - = AtomicIntegerFieldUpdater.newUpdater(SourceSubscriber.class, "sourceIndex"); - - /** Guarded by guard. */ - int active; - /** Guarded by guard. */ - final Queue> queue; - - /** Indicates the emitting phase. Guarded by this. */ - boolean emitting; - /** Counts the missed emitting calls. Guarded by this. */ - int missedEmitting; - /** The last buffer index in the round-robin drain scheme. Accessed while emitting == true. */ - int lastIndex; - - /** Guarded by itself. */ - final List subscribers; - - volatile long requested; - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED - = AtomicLongFieldUpdater.newUpdater(SourceSubscriber.class, "requested"); - - - public SourceSubscriber(int maxConcurrency, Subscriber s, CompositeSubscription csub) { - super(s); - this.maxConcurrency = maxConcurrency; - this.s = s; - this.csub = csub; - this.guard = new Object(); - this.queue = new ArrayDeque>(maxConcurrency); - this.subscribers = Collections.synchronizedList(new ArrayList()); - this.wip = 1; - } - - @Override - public void onStart() { - request(maxConcurrency); - } - - @Override - public void onNext(Observable t) { - synchronized (guard) { - queue.add(t); - } - subscribeNext(); - } - - void subscribeNext() { - Observable t; - synchronized (guard) { - t = queue.peek(); - if (t == null || active >= maxConcurrency) { - return; - } - active++; - queue.poll(); - } - - MergeItemSubscriber itemSub = new MergeItemSubscriber(SOURCE_INDEX.getAndIncrement(this)); - subscribers.add(itemSub); - - csub.add(itemSub); - - WIP.incrementAndGet(this); - - t.unsafeSubscribe(itemSub); - - request(1); - } - - @Override - public void onError(Throwable e) { - Object[] active; - synchronized (subscribers) { - active = subscribers.toArray(); - subscribers.clear(); - } - - try { - s.onError(e); - - unsubscribe(); - } finally { - for (Object o : active) { - @SuppressWarnings("unchecked") - MergeItemSubscriber a = (MergeItemSubscriber)o; - a.release(); - } - } - - } - - @Override - public void onCompleted() { - WIP.decrementAndGet(this); - drain(); - } - - protected void downstreamRequest(long n) { - for (;;) { - long r = requested; - long u; - if (r != Long.MAX_VALUE && n == Long.MAX_VALUE) { - u = Long.MAX_VALUE; - } else - if (r + n < 0) { - u = Long.MAX_VALUE; - } else { - u = r + n; - } - if (REQUESTED.compareAndSet(this, r, u)) { - break; - } - } - drain(); - } - - protected void drain() { - synchronized (this) { - if (emitting) { - missedEmitting++; - return; - } - emitting = true; - missedEmitting = 0; - } - final List.MergeItemSubscriber> subs = subscribers; - final Subscriber child = s; - Object[] active = new Object[subs.size()]; - do { - long r; - - outer: - while ((r = requested) > 0) { - int idx = lastIndex; - synchronized (subs) { - if (subs.size() == active.length) { - active = subs.toArray(active); - } else { - active = subs.toArray(); - } - } - - int resumeIndex = 0; - int j = 0; - for (Object o : active) { - @SuppressWarnings("unchecked") - MergeItemSubscriber e = (MergeItemSubscriber)o; - if (e.index == idx) { - resumeIndex = j; - break; - } - j++; - } - int sumConsumed = 0; - for (int i = 0; i < active.length; i++) { - j = (i + resumeIndex) % active.length; - - @SuppressWarnings("unchecked") - final MergeItemSubscriber e = (MergeItemSubscriber)active[j]; - final RxRingBuffer b = e.buffer; - lastIndex = e.index; - - if (!e.once && b.peek() == null) { - subs.remove(e); - - synchronized (guard) { - this.active--; - } - csub.remove(e); - - e.release(); - - subscribeNext(); - - WIP.decrementAndGet(this); - - continue outer; - } - - int consumed = 0; - Object v; - while (r > 0 && (v = b.poll()) != null) { - nl.accept(child, v); - if (child.isUnsubscribed()) { - return; - } - r--; - consumed++; - } - if (consumed > 0) { - sumConsumed += consumed; - REQUESTED.addAndGet(this, -consumed); - e.requestMore(consumed); - } - if (r == 0) { - break outer; - } - } - if (sumConsumed == 0) { - break; - } - } - - if (active.length == 0) { - if (wip == 0) { - child.onCompleted(); - return; - } - } - synchronized (this) { - if (missedEmitting == 0) { - emitting = false; - break; - } - missedEmitting = 0; - } - } while (true); - } - final class MergeItemSubscriber extends Subscriber { - volatile boolean once = true; - final int index; - final RxRingBuffer buffer; - - public MergeItemSubscriber(int index) { - buffer = RxRingBuffer.getSpmcInstance(); - this.index = index; - } - - @Override - public void onStart() { - request(RxRingBuffer.SIZE); - } - - @Override - public void onNext(T t) { - try { - buffer.onNext(t); - } catch (MissingBackpressureException ex) { - onError(ex); - return; - } - - drain(); - } - - @Override - public void onError(Throwable e) { - SourceSubscriber.this.onError(e); - } - - @Override - public void onCompleted() { - if (once) { - once = false; - drain(); - } - } - /** Request more from upstream. */ - void requestMore(long n) { - request(n); - } - void release() { - // NO-OP for now - buffer.release(); - } - } - } -} diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index c6739927ee..492cd8f261 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -759,4 +759,4 @@ public void unsubscribe() { } } } -} \ No newline at end of file +} diff --git a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java index c350c895c4..145a67096e 100644 --- a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java +++ b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java @@ -15,9 +15,12 @@ */ package rx.internal.util; -import rx.*; +import rx.Observable; +import rx.Scheduler; import rx.Scheduler.Worker; +import rx.Subscriber; import rx.functions.Action0; +import rx.functions.Func1; import rx.internal.schedulers.EventLoopsScheduler; public final class ScalarSynchronousObservable extends Observable { @@ -117,4 +120,32 @@ public void call() { subscriber.onCompleted(); } } + + public Observable scalarFlatMap(final Func1> func) { + return create(new OnSubscribe() { + @Override + public void call(final Subscriber child) { + Observable o = func.call(t); + if (o.getClass() == ScalarSynchronousObservable.class) { + child.onNext(((ScalarSynchronousObservable)o).t); + child.onCompleted(); + } else { + o.unsafeSubscribe(new Subscriber(child) { + @Override + public void onNext(R v) { + child.onNext(v); + } + @Override + public void onError(Throwable e) { + child.onError(e); + } + @Override + public void onCompleted() { + child.onCompleted(); + } + }); + } + } + }); + } } diff --git a/src/perf/java/rx/operators/OperatorFlatMapPerf.java b/src/perf/java/rx/operators/OperatorFlatMapPerf.java index c20cff67f1..2913911a0f 100644 --- a/src/perf/java/rx/operators/OperatorFlatMapPerf.java +++ b/src/perf/java/rx/operators/OperatorFlatMapPerf.java @@ -17,8 +17,8 @@ import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.BenchmarkMode; 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; diff --git a/src/test/java/rx/internal/operators/OperatorFlatMapTest.java b/src/test/java/rx/internal/operators/OperatorFlatMapTest.java index a4635f1512..bb5127665c 100644 --- a/src/test/java/rx/internal/operators/OperatorFlatMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorFlatMapTest.java @@ -17,6 +17,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; +import static org.junit.Assert.*; import java.util.*; import java.util.concurrent.TimeUnit; @@ -384,6 +385,17 @@ public Integer call(Integer t1, Integer t2) { System.out.println("--> testFlatMapSelectorMaxConcurrent: " + ts.getOnNextEvents()); Assert.assertTrue(expected.containsAll(ts.getOnNextEvents())); } + + @Test + public void testFlatMapTransformsMaxConcurrentNormalLoop() { + for (int i = 0; i < 1000; i++) { + if (i % 100 == 0) { + System.out.println("testFlatMapTransformsMaxConcurrentNormalLoop => " + i); + } + testFlatMapTransformsMaxConcurrentNormal(); + } + } + @Test public void testFlatMapTransformsMaxConcurrentNormal() { final int m = 2; @@ -416,4 +428,120 @@ public void testFlatMapTransformsMaxConcurrentNormal() { verify(o, never()).onNext(5); verify(o, never()).onError(any(Throwable.class)); } + + @Ignore // don't care for any reordering + @Test(timeout = 10000) + public void flatMapRangeAsyncLoop() { + for (int i = 0; i < 2000; i++) { + if (i % 10 == 0) { + System.out.println("flatMapRangeAsyncLoop > " + i); + } + TestSubscriber ts = new TestSubscriber(); + Observable.range(0, 1000) + .flatMap(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(t); + } + }) + .observeOn(Schedulers.computation()) + .subscribe(ts); + + ts.awaitTerminalEvent(2500, TimeUnit.MILLISECONDS); + if (ts.getOnCompletedEvents().isEmpty()) { + System.out.println(ts.getOnNextEvents().size()); + } + ts.assertTerminalEvent(); + ts.assertNoErrors(); + List list = ts.getOnNextEvents(); + assertEquals(1000, list.size()); + boolean f = false; + for (int j = 0; j < list.size(); j++) { + if (list.get(j) != j) { + System.out.println(j + " " + list.get(j)); + f = true; + } + } + if (f) { + Assert.fail("Results are out of order!"); + } + } + } + @Test(timeout = 30000) + public void flatMapRangeMixedAsyncLoop() { + for (int i = 0; i < 2000; i++) { + if (i % 10 == 0) { + System.out.println("flatMapRangeAsyncLoop > " + i); + } + TestSubscriber ts = new TestSubscriber(); + Observable.range(0, 1000) + .flatMap(new Func1>() { + final Random rnd = new Random(); + @Override + public Observable call(Integer t) { + Observable r = Observable.just(t); + if (rnd.nextBoolean()) { + r = r.asObservable(); + } + return r; + } + }) + .observeOn(Schedulers.computation()) + .subscribe(ts); + + ts.awaitTerminalEvent(2500, TimeUnit.MILLISECONDS); + if (ts.getOnCompletedEvents().isEmpty()) { + System.out.println(ts.getOnNextEvents().size()); + } + ts.assertTerminalEvent(); + ts.assertNoErrors(); + List list = ts.getOnNextEvents(); + if (list.size() < 1000) { + Set set = new HashSet(list); + for (int j = 0; j < 1000; j++) { + if (!set.contains(j)) { + System.out.println(j + " missing"); + } + } + } + assertEquals(1000, list.size()); + } + } + + @Test + public void flatMapIntPassthruAsync() { + for (int i = 0;i < 1000; i++) { + TestSubscriber ts = new TestSubscriber(); + + Observable.range(1, 1000).flatMap(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(1).subscribeOn(Schedulers.computation()); + } + }).subscribe(ts); + + ts.awaitTerminalEvent(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertCompleted(); + ts.assertValueCount(1000); + } + } + @Test + public void flatMapTwoNestedSync() { + for (final int n : new int[] { 1, 1000, 1000000 }) { + TestSubscriber ts = new TestSubscriber(); + + Observable.just(1, 2).flatMap(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.range(1, n); + } + }).subscribe(ts); + + System.out.println("flatMapTwoNestedSync >> @ " + n); + ts.assertNoErrors(); + ts.assertCompleted(); + ts.assertValueCount(n * 2); + } + } } diff --git a/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java b/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java index 85eb84b6e9..db086d6632 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java @@ -15,6 +15,17 @@ */ package rx.internal.operators; +import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; @@ -27,19 +38,9 @@ import rx.Subscriber; import rx.exceptions.CompositeException; import rx.exceptions.TestException; +import rx.functions.Action1; import rx.observers.TestSubscriber; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.*; - public class OperatorMergeDelayErrorTest { @Mock @@ -66,7 +67,9 @@ public void testErrorDelayed1() { verify(stringObserver, times(1)).onNext("four"); verify(stringObserver, times(0)).onNext("five"); // despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError - verify(stringObserver, times(1)).onNext("six"); + // inner observable errors are considered terminal for that source +// verify(stringObserver, times(1)).onNext("six"); + // inner observable errors are considered terminal for that source } @Test @@ -87,7 +90,8 @@ public void testErrorDelayed2() { verify(stringObserver, times(1)).onNext("four"); verify(stringObserver, times(0)).onNext("five"); // despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError - verify(stringObserver, times(1)).onNext("six"); + // inner observable errors are considered terminal for that source +// verify(stringObserver, times(1)).onNext("six"); verify(stringObserver, times(1)).onNext("seven"); verify(stringObserver, times(1)).onNext("eight"); verify(stringObserver, times(1)).onNext("nine"); @@ -188,7 +192,8 @@ public void testCompositeErrorDelayed1() { verify(stringObserver, times(1)).onNext("four"); verify(stringObserver, times(0)).onNext("five"); // despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError - verify(stringObserver, times(1)).onNext("six"); + // inner observable errors are considered terminal for that source +// verify(stringObserver, times(1)).onNext("six"); } @Test @@ -287,7 +292,7 @@ public void testMergeArrayWithThreading() { verify(stringObserver, times(1)).onCompleted(); } - @Test(timeout=1000L) + @Test(timeout = 1000L) public void testSynchronousError() { final Observable> o1 = Observable.error(new RuntimeException("unit test")); @@ -472,6 +477,10 @@ public void onCompleted() { }); + /* + * If the child onNext throws, why would we keep accepting values from + * other sources? + */ inOrder.verify(o).onNext(2); inOrder.verify(o, never()).onNext(0); inOrder.verify(o, never()).onNext(1); @@ -546,4 +555,26 @@ public void run() { t.start(); } } + @Test + public void testDelayErrorMaxConcurrent() { + final List requests = new ArrayList(); + Observable source = Observable.mergeDelayError(Observable.just( + Observable.just(1).asObservable(), + Observable.error(new TestException())).doOnRequest(new Action1() { + @Override + public void call(Long t1) { + requests.add(t1); + } + }), 1); + + TestSubscriber ts = new TestSubscriber(); + + source.subscribe(ts); + + ts.assertReceivedOnNext(Arrays.asList(1)); + ts.assertTerminalEvent(); + assertEquals(1, ts.getOnErrorEvents().size()); + assertTrue(ts.getOnErrorEvents().get(0) instanceof TestException); + assertEquals(Arrays.asList(1L, 1L, 1L), requests); + } } \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java index 52c7ee21f2..9cd65de8d0 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java @@ -190,7 +190,7 @@ public void testSimpleOneLess() { ts.assertReceivedOnNext(result); } } - @Test(timeout = 10000) + @Test(timeout = 20000) public void testSimpleAsyncLoop() { for (int i = 0; i < 200; i++) { testSimpleAsync(); diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 7d785b4088..9732611e44 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -16,46 +16,26 @@ package rx.internal.operators; import static java.util.Arrays.asList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; -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.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Notification; -import rx.Observable; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.*; + +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Scheduler; import rx.Scheduler.Worker; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.Observable; +import rx.Observer; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.schedulers.TestScheduler; +import rx.schedulers.*; import rx.subscriptions.Subscriptions; public class OperatorMergeTest { @@ -494,7 +474,7 @@ public void call() { }); } - @Test(timeout = 10000) + @Test//(timeout = 10000) public void testConcurrency() { Observable o = Observable.range(1, 10000).subscribeOn(Schedulers.newThread()); @@ -503,7 +483,8 @@ public void testConcurrency() { TestSubscriber ts = new TestSubscriber(); merge.subscribe(ts); - ts.awaitTerminalEvent(); + ts.awaitTerminalEvent(3, TimeUnit.SECONDS); + ts.assertTerminalEvent(); ts.assertNoErrors(); assertEquals(1, ts.getOnCompletedEvents().size()); List onNextEvents = ts.getOnNextEvents(); @@ -672,7 +653,7 @@ public void onNext(Integer t) { * * This requires merge to also obey the Product.request values coming from it's child subscriber. */ - @Test + @Test(timeout = 10000) public void testBackpressureDownstreamWithConcurrentStreams() throws InterruptedException { final AtomicInteger generated1 = new AtomicInteger(); Observable o1 = createInfiniteObservable(generated1).subscribeOn(Schedulers.computation()); @@ -1055,8 +1036,9 @@ public void shouldNotCompleteIfThereArePendingScalarSynchronousEmissionsWhenTheL assertEquals(Collections.>emptyList(), subscriber.getOnCompletedEvents()); subscriber.requestMore(1); subscriber.assertReceivedOnNext(asList(1L)); - assertEquals(Collections.>emptyList(), subscriber.getOnCompletedEvents()); - subscriber.requestMore(1); +// TODO: it should be acceptable to get a completion event without requests +// assertEquals(Collections.>emptyList(), subscriber.getOnCompletedEvents()); +// subscriber.requestMore(1); subscriber.assertTerminalEvent(); } @@ -1240,4 +1222,85 @@ public void call(Integer s) { } }; } + + Func1> toScalar = new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(t); + } + }; + Func1> toHiddenScalar = new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(t).asObservable(); + } + }; + + void runMerge(Func1> func, TestSubscriber ts) { + List list = new ArrayList(); + for (int i = 0; i < 1000; i++) { + list.add(i); + } + Observable source = Observable.from(list); + source.flatMap(func).subscribe(ts); + + if (ts.getOnNextEvents().size() != 1000) { + System.out.println(ts.getOnNextEvents()); + } + + ts.assertTerminalEvent(); + ts.assertNoErrors(); + ts.assertReceivedOnNext(list); + } + + @Test + public void testFastMergeFullScalar() { + runMerge(toScalar, new TestSubscriber()); + } + @Test + public void testFastMergeHiddenScalar() { + runMerge(toHiddenScalar, new TestSubscriber()); + } + @Test + public void testSlowMergeFullScalar() { + for (final int req : new int[] { 16, 32, 64, 128, 256 }) { + TestSubscriber ts = new TestSubscriber() { + int remaining = req; + @Override + public void onStart() { + request(req); + } + @Override + public void onNext(Integer t) { + super.onNext(t); + if (--remaining == 0) { + remaining = req; + request(req); + } + } + }; + runMerge(toScalar, ts); + } + } + @Test + public void testSlowMergeHiddenScalar() { + for (final int req : new int[] { 16, 32, 64, 128, 256 }) { + TestSubscriber ts = new TestSubscriber() { + int remaining = req; + @Override + public void onStart() { + request(req); + } + @Override + public void onNext(Integer t) { + super.onNext(t); + if (--remaining == 0) { + remaining = req; + request(req); + } + } + }; + runMerge(toHiddenScalar, ts); + } + } } From 96786bb520badde87b74be0120d4473c1c259a3f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 14 Jul 2015 12:09:06 -0700 Subject: [PATCH 327/857] Binary examples including SNAPSHOT --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a590c9d925..9014a45c7e 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,13 @@ All code inside the `rx.internal.*` packages is considered private API and shoul Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cio.reactivex.rxjava). -Example for Maven: +Example for Gradle: + +```groovy +compile 'io.reactivex:rxjava:x.y.z' +``` + +and for Maven: ```xml @@ -73,6 +79,18 @@ and for Ivy: ``` +Snapshots are available via [JFrog](https://oss.jfrog.org/webapp/search/artifact/?5&q=rxjava): + +```groovy +repositories { + maven { url 'https://oss.jfrog.org/libs-snapshot' } +} + +dependencies { + compile 'io.reactivex:rxjava:1.0.y-SNAPSHOT' +} +``` + ## Build To build: From 3deba11f33e8cdca2fd7b84d8acc14ec26e41c99 Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 14 Jul 2015 13:23:54 -0700 Subject: [PATCH 328/857] Add "since" annotations to javadocs for new Experimental/Beta methods --- src/main/java/rx/Observable.java | 6 ++++++ src/main/java/rx/observers/Subscribers.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 11b6b43f00..3d9245df87 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1749,6 +1749,7 @@ public final static Observable merge(ObservableReactiveX operators documentation: Merge + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental @SuppressWarnings({"unchecked", "rawtypes"}) @@ -2047,11 +2048,13 @@ public final static Observable merge(Observable[] sequences) * the maximum number of Observables that may be subscribed to concurrently * @return an Observable that emits all of the items emitted by the Observables in the Array * @see ReactiveX operators documentation: Merge + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public final static Observable merge(Observable[] sequences, int maxConcurrent) { return merge(from(sequences), maxConcurrent); } + /** * Flattens an Observable that emits Observables into one Observable, in a way that allows an Observer to * receive all successfully emitted items from all of the source Observables without being interrupted by @@ -2079,6 +2082,7 @@ public final static Observable merge(Observable[] sequences, public final static Observable mergeDelayError(Observable> source) { return source.lift(OperatorMerge.instance(true)); } + /** * Flattens an Observable that emits Observables into one Observable, in a way that allows an Observer to * receive all successfully emitted items from all of the source Observables without being interrupted by @@ -2105,6 +2109,7 @@ public final static Observable mergeDelayError(ObservableReactiveX operators documentation: Merge + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public final static Observable mergeDelayError(Observable> source, int maxConcurrent) { @@ -5513,6 +5518,7 @@ public final Observable onBackpressureDrop() { public final Observable onBackpressureBlock(int maxQueueLength) { return lift(new OperatorOnBackpressureBlock(maxQueueLength)); } + /** * Instructs an Observable that is emitting items faster than its observer can consume them to block the * producer thread if the number of undelivered onNext events reaches the system-wide ring buffer size. diff --git a/src/main/java/rx/observers/Subscribers.java b/src/main/java/rx/observers/Subscribers.java index 1c42aa4b68..4e81c1af8d 100644 --- a/src/main/java/rx/observers/Subscribers.java +++ b/src/main/java/rx/observers/Subscribers.java @@ -213,7 +213,7 @@ public final void onNext(T args) { * subscriber, has backpressure controlled by * subscriber and uses subscriber to * manage unsubscription. - * + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public static Subscriber wrap(final Subscriber subscriber) { From 899e6a8f2a9d5d39b769ac17911dec0c32070c9c Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 15 Jul 2015 13:16:33 +1000 Subject: [PATCH 329/857] fix forEach javadoc --- src/main/java/rx/Observable.java | 15 +++++++++------ src/test/java/rx/ObservableTests.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 11b6b43f00..71313c657c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4940,9 +4940,9 @@ public final Observable flatMapIterable(Func1ReactiveX operators documentation: Subscribe */ public final void forEach(final Action1 onNext) { @@ -4964,8 +4964,9 @@ public final void forEach(final Action1 onNext) { * {@link Action1} to execute when an error is emitted. * @throws IllegalArgumentException * if {@code onNext} is null, or - * if {@code onError} is null, or - * if {@code onComplete} is null + * if {@code onError} is null + * @throws OnErrorNotImplementedException + * if the Observable calls {@code onError} * @see ReactiveX operators documentation: Subscribe */ public final void forEach(final Action1 onNext, final Action1 onError) { @@ -4991,6 +4992,8 @@ public final void forEach(final Action1 onNext, final Action1ReactiveX operators documentation: Subscribe */ public final void forEach(final Action1 onNext, final Action1 onError, final Action0 onComplete) { @@ -7485,7 +7488,7 @@ public final void onNext(T args) { * @throws IllegalArgumentException * if {@code onNext} is null * @throws OnErrorNotImplementedException - * if the Observable tries to call {@code onError} + * if the Observable calls {@code onError} * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(final Action1 onNext) { diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index badbfe262e..5f1667deb6 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -53,6 +53,7 @@ import rx.functions.Func0; import rx.functions.Func1; import rx.functions.Func2; +import rx.functions.Functions; import rx.observables.ConnectableObservable; import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; @@ -1138,4 +1139,22 @@ public void testSubscribingSubscriberAsObserverMaintainsSubscriptionChain() { subscriber.assertUnsubscribed(); } + + @Test(expected=OnErrorNotImplementedException.class) + public void testForEachWithError() { + Observable.error(new Exception("boo")) + // + .forEach(new Action1() { + @Override + public void call(Object t) { + //do nothing + }}); + } + + @Test(expected=IllegalArgumentException.class) + public void testForEachWithNull() { + Observable.error(new Exception("boo")) + // + .forEach(null); + } } From 8d1cecc95d5c58b7e0521f1037d30402d3931e80 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 17 Jul 2015 09:36:48 +1000 Subject: [PATCH 330/857] break tests as approach timeout so that don't fail on slow machines --- src/test/java/rx/BackpressureTests.java | 6 ++++++ .../internal/operators/OperatorMergeMaxConcurrentTest.java | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index ffa2e01129..e948aa07f0 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -419,7 +419,13 @@ public void testFirehoseFailsAsExpected() { @Test(timeout = 10000) public void testOnBackpressureDrop() { + long t = System.currentTimeMillis(); for (int i = 0; i < 100; i++) { + // stop the test if we are getting close to the timeout because slow machines + // may not get through 100 iterations + if (System.currentTimeMillis() - t > TimeUnit.SECONDS.toMillis(9)) { + break; + } int NUM = (int) (RxRingBuffer.SIZE * 1.1); // > 1 so that take doesn't prevent buffer overflow AtomicInteger c = new AtomicInteger(); TestSubscriber ts = new TestSubscriber(); diff --git a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java index 9cd65de8d0..af20d14316 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java @@ -224,7 +224,11 @@ public void testSimpleOneLessAsyncLoop() { } @Test(timeout = 10000) public void testSimpleOneLessAsync() { + long t = System.currentTimeMillis(); for (int i = 2; i < 50; i++) { + if (System.currentTimeMillis() - t > TimeUnit.SECONDS.toMillis(9)) { + break; + } TestSubscriber ts = new TestSubscriber(); List> sourceList = new ArrayList>(i); Set expected = new HashSet(i); From e25cd27e403f0be84a93e7810ccc2a0675727c7d Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 17 Jul 2015 09:49:46 +1000 Subject: [PATCH 331/857] reduce probability of ExecutorSchedulerTest.testOnBackpressureDrop failing on slow machine --- src/test/java/rx/schedulers/ExecutorSchedulerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java index b11f7879e1..cdefabc757 100644 --- a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java +++ b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java @@ -177,11 +177,11 @@ public void execute(Runnable command) { }; ExecutorSchedulerWorker w = (ExecutorSchedulerWorker)Schedulers.from(e).createWorker(); - w.schedule(Actions.empty(), 1, TimeUnit.MILLISECONDS); + w.schedule(Actions.empty(), 50, TimeUnit.MILLISECONDS); assertTrue(w.tasks.hasSubscriptions()); - Thread.sleep(100); + Thread.sleep(150); assertFalse(w.tasks.hasSubscriptions()); } From 2f7031e780404c8f5a355809dd309885f27ba483 Mon Sep 17 00:00:00 2001 From: George Campbell Date: Fri, 17 Jul 2015 16:27:48 -0700 Subject: [PATCH 332/857] Revert "No need to allocate a new head node." This reverts commit 46f9138f509f22be61d435cfb79335396fc92c48. --- .../rx/internal/operators/OperatorReplay.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index 2989f50b9e..77f19edf32 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -897,9 +897,9 @@ final void removeFirst() { size--; // can't just move the head because it would retain the very first value // can't null out the head's value because of late replayers would see null - setFirst(next); + setFirst(next.get()); } - /* test */ final void removeSome(int n) { + final void removeSome(int n) { Node head = get(); while (n > 0) { head = head.get(); @@ -907,14 +907,19 @@ final void removeFirst() { size--; } - setFirst(head); + setFirst(head.get()); } /** * Arranges the given node is the new head from now on. * @param n */ final void setFirst(Node n) { - set(n); + Node newHead = new Node(null); + newHead.lazySet(n); + if (n == null) { + tail = newHead; + } + set(newHead); } @Override @@ -1114,8 +1119,8 @@ Object leaveTransform(Object value) { void truncate() { long timeLimit = scheduler.now() - maxAgeInMillis; - Node prev = get(); - Node next = prev.get(); + Node head = get(); + Node next = head.get(); int e = 0; for (;;) { @@ -1123,14 +1128,12 @@ void truncate() { if (size > limit) { e++; size--; - prev = next; next = next.get(); } else { Timestamped v = (Timestamped)next.value; if (v.getTimestampMillis() <= timeLimit) { e++; size--; - prev = next; next = next.get(); } else { break; @@ -1141,15 +1144,15 @@ void truncate() { } } if (e != 0) { - setFirst(prev); + setFirst(next); } } @Override void truncateFinal() { long timeLimit = scheduler.now() - maxAgeInMillis; - Node prev = get(); - Node next = prev.get(); + Node head = get(); + Node next = head.get(); int e = 0; for (;;) { @@ -1158,7 +1161,6 @@ void truncateFinal() { if (v.getTimestampMillis() <= timeLimit) { e++; size--; - prev = next; next = next.get(); } else { break; @@ -1168,7 +1170,7 @@ void truncateFinal() { } } if (e != 0) { - setFirst(prev); + setFirst(next); } } } From 47644d3c7e111a81863cd2f1b30257da7295f89f Mon Sep 17 00:00:00 2001 From: George Campbell Date: Fri, 17 Jul 2015 16:27:55 -0700 Subject: [PATCH 333/857] Revert "Operator replay() now supports backpressure" This reverts commit 82d7b9cca2efd0a8f36ec3b700bb8f34c445a093. --- src/main/java/rx/Observable.java | 213 +-- .../rx/internal/operators/OperatorReplay.java | 1176 +---------------- .../operators/OperatorReplayTest.java | 160 +-- 3 files changed, 195 insertions(+), 1354 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 11b6b43f00..9e8b7db2d8 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -25,6 +25,7 @@ import rx.observers.SafeSubscriber; import rx.plugins.*; import rx.schedulers.*; +import rx.subjects.*; import rx.subscriptions.Subscriptions; /** @@ -5975,9 +5976,9 @@ public Void call(Notification notification) { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -5987,7 +5988,14 @@ public Void call(Notification notification) { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay() { - return OperatorReplay.create(this); + return new OperatorMulticast(this, new Func0>() { + + @Override + public Subject call() { + return ReplaySubject. create(); + } + + }); } /** @@ -5997,9 +6005,9 @@ public final ConnectableObservable replay() { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -6014,12 +6022,12 @@ public final ConnectableObservable replay() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector) { - return OperatorReplay.multicastSelector(new Func0>() { + return create(new OnSubscribeMulticastSelector(this, new Func0>() { @Override - public ConnectableObservable call() { - return Observable.this.replay(); + public final Subject call() { + return ReplaySubject.create(); } - }, selector); + }, selector)); } /** @@ -6030,9 +6038,9 @@ public ConnectableObservable call() { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -6050,12 +6058,12 @@ public ConnectableObservable call() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize) { - return OperatorReplay.multicastSelector(new Func0>() { + return create(new OnSubscribeMulticastSelector(this, new Func0>() { @Override - public ConnectableObservable call() { - return Observable.this.replay(bufferSize); + public final Subject call() { + return ReplaySubject.createWithSize(bufferSize); } - }, selector); + }, selector)); } /** @@ -6066,9 +6074,9 @@ public ConnectableObservable call() { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6102,9 +6110,9 @@ public final Observable replay(Func1, ? extends Obs * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6134,12 +6142,12 @@ public final Observable replay(Func1, ? extends Obs if (bufferSize < 0) { throw new IllegalArgumentException("bufferSize < 0"); } - return OperatorReplay.multicastSelector(new Func0>() { + return create(new OnSubscribeMulticastSelector(this, new Func0>() { @Override - public ConnectableObservable call() { - return Observable.this.replay(bufferSize, time, unit, scheduler); + public final Subject call() { + return ReplaySubject.createWithTimeAndSize(time, unit, bufferSize, scheduler); } - }, selector); + }, selector)); } /** @@ -6150,9 +6158,9 @@ public ConnectableObservable call() { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6171,18 +6179,13 @@ public ConnectableObservable call() { * replaying no more than {@code bufferSize} notifications * @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>() { + public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize, final Scheduler scheduler) { + return create(new OnSubscribeMulticastSelector(this, new Func0>() { @Override - public ConnectableObservable call() { - return Observable.this.replay(bufferSize); + public final Subject call() { + return OperatorReplay. createScheduledSubject(ReplaySubject.createWithSize(bufferSize), scheduler); } - }, new Func1, Observable>() { - @Override - public Observable call(Observable t) { - return selector.call(t).observeOn(scheduler); - } - }); + }, selector)); } /** @@ -6193,9 +6196,9 @@ public Observable call(Observable t) { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6226,9 +6229,9 @@ public final Observable replay(Func1, ? extends Obs * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6250,12 +6253,12 @@ 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>() { + return create(new OnSubscribeMulticastSelector(this, new Func0>() { @Override - public ConnectableObservable call() { - return Observable.this.replay(time, unit, scheduler); + public final Subject call() { + return ReplaySubject.createWithTime(time, unit, scheduler); } - }, selector); + }, selector)); } /** @@ -6265,9 +6268,9 @@ public ConnectableObservable call() { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6284,18 +6287,13 @@ public ConnectableObservable call() { * replaying all items * @see ReactiveX operators documentation: Replay */ - public final Observable replay(final Func1, ? extends Observable> selector, final Scheduler scheduler) { - return OperatorReplay.multicastSelector(new Func0>() { + public final Observable replay(Func1, ? extends Observable> selector, final Scheduler scheduler) { + return create(new OnSubscribeMulticastSelector(this, new Func0>() { @Override - public ConnectableObservable call() { - return Observable.this.replay(); + public final Subject call() { + return OperatorReplay.createScheduledSubject(ReplaySubject. create(), scheduler); } - }, new Func1, Observable>() { - @Override - public Observable call(Observable t) { - return selector.call(t).observeOn(scheduler); - } - }); + }, selector)); } /** @@ -6307,9 +6305,9 @@ public Observable call(Observable t) { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -6321,7 +6319,14 @@ public Observable call(Observable t) { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize) { - return OperatorReplay.create(this, bufferSize); + return new OperatorMulticast(this, new Func0>() { + + @Override + public Subject call() { + return ReplaySubject.createWithSize(bufferSize); + } + + }); } /** @@ -6333,9 +6338,9 @@ public final ConnectableObservable replay(final int bufferSize) { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6364,9 +6369,9 @@ public final ConnectableObservable replay(int bufferSize, long time, TimeUnit * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6390,7 +6395,14 @@ public final ConnectableObservable replay(final int bufferSize, final long ti if (bufferSize < 0) { throw new IllegalArgumentException("bufferSize < 0"); } - return OperatorReplay.create(this, time, unit, scheduler, bufferSize); + return new OperatorMulticast(this, new Func0>() { + + @Override + public Subject call() { + return ReplaySubject.createWithTimeAndSize(time, unit, bufferSize, scheduler); + } + + }); } /** @@ -6402,9 +6414,9 @@ public final ConnectableObservable replay(final int bufferSize, final long ti * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6418,7 +6430,14 @@ public final ConnectableObservable replay(final int bufferSize, final long ti * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize, final Scheduler scheduler) { - return OperatorReplay.observeOn(replay(bufferSize), scheduler); + return new OperatorMulticast(this, new Func0>() { + + @Override + public Subject call() { + return OperatorReplay.createScheduledSubject(ReplaySubject.createWithSize(bufferSize), scheduler); + } + + }); } /** @@ -6430,9 +6449,9 @@ public final ConnectableObservable replay(final int bufferSize, final Schedul * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6458,9 +6477,9 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6476,7 +6495,14 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final long time, final TimeUnit unit, final Scheduler scheduler) { - return OperatorReplay.create(this, time, unit, scheduler); + return new OperatorMulticast(this, new Func0>() { + + @Override + public Subject call() { + return ReplaySubject.createWithTime(time, unit, scheduler); + } + + }); } /** @@ -6488,9 +6514,9 @@ public final ConnectableObservable replay(final long time, final TimeUnit uni * *
*
Backpressure Support:
- *
This operator supports backpressure. Note that the upstream requests are determined by the child - * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will - * request 100 elements from the underlying Observable sequence.
+ *
This operator does not support backpressure because multicasting means the stream is "hot" with + * multiple subscribers. Each child will need to manage backpressure independently using operators such + * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6503,7 +6529,14 @@ public final ConnectableObservable replay(final long time, final TimeUnit uni * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final Scheduler scheduler) { - return OperatorReplay.observeOn(replay(), scheduler); + return new OperatorMulticast(this, new Func0>() { + + @Override + public Subject call() { + return OperatorReplay.createScheduledSubject(ReplaySubject. create(), scheduler); + } + + }); } /** diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index 77f19edf32..83c76dfe39 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -15,1163 +15,93 @@ */ package rx.internal.operators; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.*; -import rx.*; import rx.Observable; -import rx.exceptions.Exceptions; -import rx.functions.*; -import rx.observables.ConnectableObservable; -import rx.schedulers.Timestamped; -import rx.subscriptions.Subscriptions; +import rx.Observable.OnSubscribe; +import rx.Scheduler; +import rx.Subscriber; +import rx.subjects.Subject; -public final class OperatorReplay extends ConnectableObservable { - /** The source observable. */ - final Observable source; - /** Holds the current subscriber that is, will be or just was subscribed to the source observable. */ - final AtomicReference> current; - /** A factory that creates the appropriate buffer for the ReplaySubscriber. */ - final Func0> bufferFactory; - - @SuppressWarnings("rawtypes") - static final Func0 DEFAULT_UNBOUNDED_FACTORY = new Func0() { - @Override - public Object call() { - return new UnboundedReplayBuffer(16); - } - }; - - /** - * Given a connectable observable factory, it multicasts over the generated - * ConnectableObservable via a selector function. - * @param connectableFactory - * @param selector - * @return - */ - public static Observable multicastSelector( - final Func0> connectableFactory, - final Func1, ? extends Observable> selector) { - return Observable.create(new OnSubscribe() { - @Override - public void call(final Subscriber child) { - ConnectableObservable co; - Observable observable; - try { - co = connectableFactory.call(); - observable = selector.call(co); - } catch (Throwable e) { - Exceptions.throwIfFatal(e); - child.onError(e); - return; - } - - observable.subscribe(child); - - co.connect(new Action1() { - @Override - public void call(Subscription t) { - child.add(t); - } - }); - } - }); - } - - /** - * Child Subscribers will observe the events of the ConnectableObservable on the - * specified scheduler. - * @param co - * @param scheduler - * @return - */ - public static ConnectableObservable observeOn(final ConnectableObservable co, final Scheduler scheduler) { - final Observable observable = co.observeOn(scheduler); - OnSubscribe onSubscribe = new OnSubscribe() { - @Override - public void call(final Subscriber child) { - // apply observeOn and prevent calling onStart() again - observable.unsafeSubscribe(new Subscriber(child) { - @Override - public void onNext(T t) { - child.onNext(t); - } - @Override - public void onError(Throwable e) { - child.onError(e); - } - @Override - public void onCompleted() { - child.onCompleted(); - } - }); - } - }; - return new ConnectableObservable(onSubscribe) { - @Override - public void connect(Action1 connection) { - co.connect(connection); - } - }; - } - - /** - * Creates a replaying ConnectableObservable with an unbounded buffer. - * @param source - * @return - */ - @SuppressWarnings("unchecked") - public static ConnectableObservable create(Observable source) { - return create(source, DEFAULT_UNBOUNDED_FACTORY); - } - - /** - * Creates a replaying ConnectableObservable with a size bound buffer. - * @param source - * @param bufferSize - * @return - */ - public static ConnectableObservable create(Observable source, - final int bufferSize) { - if (bufferSize == Integer.MAX_VALUE) { - return create(source); - } - return create(source, new Func0>() { - @Override - public ReplayBuffer call() { - return new SizeBoundReplayBuffer(bufferSize); - } - }); +/** + * Replay with limited buffer and/or time constraints. + * + * + * @see MSDN: Observable.Replay overloads + */ +public final class OperatorReplay { + /** Utility class. */ + private OperatorReplay() { + throw new IllegalStateException("No instances!"); } /** - * Creates a replaying ConnectableObservable with a time bound buffer. - * @param source - * @param maxAge - * @param unit - * @param scheduler - * @return + * Creates a subject whose client observers will observe events + * propagated through the given wrapped subject. + * @param the element type + * @param subject the subject to wrap + * @param scheduler the target scheduler + * @return the created subject */ - public static ConnectableObservable create(Observable source, - long maxAge, TimeUnit unit, Scheduler scheduler) { - return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE); - } + public static Subject createScheduledSubject(Subject subject, Scheduler scheduler) { + final Observable observedOn = subject.observeOn(scheduler); + SubjectWrapper s = new SubjectWrapper(new OnSubscribe() { - /** - * Creates a replaying ConnectableObservable with a size and time bound buffer. - * @param source - * @param maxAge - * @param unit - * @param scheduler - * @param bufferSize - * @return - */ - public static ConnectableObservable create(Observable source, - long maxAge, TimeUnit unit, final Scheduler scheduler, final int bufferSize) { - final long maxAgeInMillis = unit.toMillis(maxAge); - return create(source, new Func0>() { @Override - public ReplayBuffer call() { - return new SizeAndTimeBoundReplayBuffer(bufferSize, maxAgeInMillis, scheduler); + public void call(Subscriber o) { + subscriberOf(observedOn).call(o); } - }); + + }, subject); + return s; } /** - * Creates a OperatorReplay instance to replay values of the given source observable. - * @param source the source observable - * @param bufferFactory the factory to instantiate the appropriate buffer when the observable becomes active - * @return the connectable observable + * Return an OnSubscribeFunc which delegates the subscription to the given observable. + * + * @param the value type + * @param target the target observable + * @return the function that delegates the subscription to the target */ - static ConnectableObservable create(Observable source, - final Func0> bufferFactory) { - // the current connection to source needs to be shared between the operator and its onSubscribe call - final AtomicReference> curr = new AtomicReference>(); - OnSubscribe onSubscribe = new OnSubscribe() { + public static OnSubscribe subscriberOf(final Observable target) { + return new OnSubscribe() { @Override - public void call(Subscriber child) { - // concurrent connection/disconnection may change the state, - // we loop to be atomic while the child subscribes - for (;;) { - // get the current subscriber-to-source - ReplaySubscriber r = curr.get(); - // if there isn't one - if (r == null) { - // create a new subscriber to source - ReplaySubscriber u = new ReplaySubscriber(curr, bufferFactory.call()); - // perform extra initialization to avoid 'this' to escape during construction - u.init(); - // let's try setting it as the current subscriber-to-source - if (!curr.compareAndSet(r, u)) { - // didn't work, maybe someone else did it or the current subscriber - // to source has just finished - continue; - } - // we won, let's use it going onwards - r = u; - } - - // create the backpressure-managing producer for this child - InnerProducer inner = new InnerProducer(r, child); - // we try to add it to the array of producers - // if it fails, no worries because we will still have its buffer - // so it is going to replay it for us - r.add(inner); - // the producer has been registered with the current subscriber-to-source so - // at least it will receive the next terminal event - child.add(inner); - // setting the producer will trigger the first request to be considered by - // the subscriber-to-source. - child.setProducer(inner); - break; - } + public void call(Subscriber t1) { + target.unsafeSubscribe(t1); } }; - return new OperatorReplay(onSubscribe, source, curr, bufferFactory); - } - private OperatorReplay(OnSubscribe onSubscribe, Observable source, - final AtomicReference> current, - final Func0> bufferFactory) { - super(onSubscribe); - this.source = source; - this.current = current; - this.bufferFactory = bufferFactory; - } - - @Override - public void connect(Action1 connection) { - boolean doConnect = false; - ReplaySubscriber ps; - // we loop because concurrent connect/disconnect and termination may change the state - for (;;) { - // retrieve the current subscriber-to-source instance - ps = current.get(); - // if there is none yet or the current has unsubscribed - if (ps == null || ps.isUnsubscribed()) { - // create a new subscriber-to-source - ReplaySubscriber u = new ReplaySubscriber(current, bufferFactory.call()); - // initialize out the constructor to avoid 'this' to escape - u.init(); - // try setting it as the current subscriber-to-source - if (!current.compareAndSet(ps, u)) { - // did not work, perhaps a new subscriber arrived - // and created a new subscriber-to-source as well, retry - continue; - } - ps = u; - } - // if connect() was called concurrently, only one of them should actually - // connect to the source - doConnect = !ps.shouldConnect.get() && ps.shouldConnect.compareAndSet(false, true); - break; - } - /* - * Notify the callback that we have a (new) connection which it can unsubscribe - * but since ps is unique to a connection, multiple calls to connect() will return the - * same Subscription and even if there was a connect-disconnect-connect pair, the older - * references won't disconnect the newer connection. - * Synchronous source consumers have the opportunity to disconnect via unsubscribe on the - * Subscription as unsafeSubscribe may never return in its own. - * - * Note however, that asynchronously disconnecting a running source might leave - * child-subscribers without any terminal event; ReplaySubject does not have this - * issue because the unsubscription was always triggered by the child-subscribers - * themselves. - */ - connection.call(ps); - if (doConnect) { - source.unsafeSubscribe(ps); - } } - - @SuppressWarnings("rawtypes") - static final class ReplaySubscriber extends Subscriber implements Subscription { - /** Holds notifications from upstream. */ - final ReplayBuffer buffer; - /** The notification-lite factory. */ - final NotificationLite nl; - /** Contains either an onCompleted or an onError token from upstream. */ - boolean done; - - /** Indicates an empty array of inner producers. */ - static final InnerProducer[] EMPTY = new InnerProducer[0]; - /** Indicates a terminated ReplaySubscriber. */ - static final InnerProducer[] TERMINATED = new InnerProducer[0]; - - /** Tracks the subscribed producers. */ - final AtomicReference producers; - /** - * Atomically changed from false to true by connect to make sure the - * connection is only performed by one thread. - */ - final AtomicBoolean shouldConnect; - - /** Guarded by this. */ - boolean emitting; - /** Guarded by this. */ - boolean missed; - - - /** Contains the maximum element index the child Subscribers requested so far. Accessed while emitting is true. */ - long maxChildRequested; - /** Counts the outstanding upstream requests until the producer arrives. */ - long maxUpstreamRequested; - /** The upstream producer. */ - volatile Producer producer; - - public ReplaySubscriber(AtomicReference> current, - ReplayBuffer buffer) { - this.buffer = buffer; - - this.nl = NotificationLite.instance(); - this.producers = new AtomicReference(EMPTY); - this.shouldConnect = new AtomicBoolean(); - // make sure the source doesn't produce values until the child subscribers - // expressed their request amounts - this.request(0); - } - /** Should be called after the constructor finished to setup nulling-out the current reference. */ - void init() { - add(Subscriptions.create(new Action0() { - @Override - public void call() { - ReplaySubscriber.this.producers.getAndSet(TERMINATED); - // unlike OperatorPublish, we can't null out the terminated so - // late subscribers can still get replay - // current.compareAndSet(ReplaySubscriber.this, null); - // we don't care if it fails because it means the current has - // been replaced in the meantime - } - })); - } - /** - * Atomically try adding a new InnerProducer to this Subscriber or return false if this - * Subscriber was terminated. - * @param producer the producer to add - * @return true if succeeded, false otherwise - */ - boolean add(InnerProducer producer) { - if (producer == null) { - throw new NullPointerException(); - } - // the state can change so we do a CAS loop to achieve atomicity - for (;;) { - // get the current producer array - InnerProducer[] c = producers.get(); - // if this subscriber-to-source reached a terminal state by receiving - // an onError or onCompleted, just refuse to add the new producer - if (c == TERMINATED) { - return false; - } - // we perform a copy-on-write logic - int len = c.length; - InnerProducer[] u = new InnerProducer[len + 1]; - System.arraycopy(c, 0, u, 0, len); - u[len] = producer; - // try setting the producers array - if (producers.compareAndSet(c, u)) { - return true; - } - // if failed, some other operation succeded (another add, remove or termination) - // so retry - } - } - - /** - * Atomically removes the given producer from the producers array. - * @param producer the producer to remove - */ - void remove(InnerProducer producer) { - // the state can change so we do a CAS loop to achieve atomicity - for (;;) { - // let's read the current producers array - InnerProducer[] c = producers.get(); - // if it is either empty or terminated, there is nothing to remove so we quit - if (c == EMPTY || c == TERMINATED) { - return; - } - // let's find the supplied producer in the array - // although this is O(n), we don't expect too many child subscribers in general - int j = -1; - int len = c.length; - for (int i = 0; i < len; i++) { - if (c[i].equals(producer)) { - j = i; - break; - } - } - // we didn't find it so just quit - if (j < 0) { - return; - } - // we do copy-on-write logic here - InnerProducer[] u; - // we don't create a new empty array if producer was the single inhabitant - // but rather reuse an empty array - if (len == 1) { - u = EMPTY; - } else { - // otherwise, create a new array one less in size - u = new InnerProducer[len - 1]; - // copy elements being before the given producer - System.arraycopy(c, 0, u, 0, j); - // copy elements being after the given producer - System.arraycopy(c, j + 1, u, j, len - j - 1); - } - // try setting this new array as - if (producers.compareAndSet(c, u)) { - return; - } - // if we failed, it means something else happened - // (a concurrent add/remove or termination), we need to retry - } - } - - @Override - public void setProducer(Producer p) { - Producer p0 = producer; - if (p0 != null) { - throw new IllegalStateException("Only a single producer can be set on a Subscriber."); - } - producer = p; - manageRequests(); - replay(); - } - - @Override - public void onNext(T t) { - if (!done) { - buffer.next(t); - replay(); - } - } - @Override - public void onError(Throwable e) { - // The observer front is accessed serially as required by spec so - // no need to CAS in the terminal value - if (!done) { - done = true; - try { - buffer.error(e); - replay(); - } finally { - unsubscribe(); // expectation of testIssue2191 - } - } - } - @Override - public void onCompleted() { - // The observer front is accessed serially as required by spec so - // no need to CAS in the terminal value - if (!done) { - done = true; - try { - buffer.complete(); - replay(); - } finally { - unsubscribe(); - } - } - } - - /** - * Coordinates the request amounts of various child Subscribers. - */ - void manageRequests() { - // if the upstream has completed, no more requesting is possible - if (isUnsubscribed()) { - return; - } - synchronized (this) { - if (emitting) { - missed = true; - return; - } - emitting = true; - } - for (;;) { - // if the upstream has completed, no more requesting is possible - if (isUnsubscribed()) { - return; - } - - @SuppressWarnings("unchecked") - InnerProducer[] a = producers.get(); - - long ri = maxChildRequested; - long maxTotalRequests = 0; - - for (InnerProducer rp : a) { - maxTotalRequests = Math.max(maxTotalRequests, rp.totalRequested.get()); - } - - long ur = maxUpstreamRequested; - Producer p = producer; - long diff = maxTotalRequests - ri; - if (diff != 0) { - maxChildRequested = maxTotalRequests; - if (p != null) { - if (ur != 0L) { - maxUpstreamRequested = 0L; - p.request(ur + diff); - } else { - p.request(diff); - } - } else { - // collect upstream request amounts until there is a producer for them - long u = ur + diff; - if (u < 0) { - u = Long.MAX_VALUE; - } - maxUpstreamRequested = u; - } - } else - // if there were outstanding upstream requests and we have a producer - if (ur != 0L && p != null) { - maxUpstreamRequested = 0L; - // fire the accumulated requests - p.request(ur); - } - - synchronized (this) { - if (!missed) { - emitting = false; - return; - } - missed = false; - } - } - } - - /** - * Tries to replay the buffer contents to all known subscribers. - */ - void replay() { - @SuppressWarnings("unchecked") - InnerProducer[] a = producers.get(); - for (InnerProducer rp : a) { - buffer.replay(rp); - } - } - } - /** - * A Producer and Subscription that manages the request and unsubscription state of a - * child subscriber in thread-safe manner. - * We use AtomicLong as a base class to save on extra allocation of an AtomicLong and also - * save the overhead of the AtomicIntegerFieldUpdater. - * @param the value type - */ - static final class InnerProducer extends AtomicLong implements Producer, Subscription { - /** */ - private static final long serialVersionUID = -4453897557930727610L; - /** - * The parent subscriber-to-source used to allow removing the child in case of - * child unsubscription. - */ - final ReplaySubscriber parent; - /** The actual child subscriber. */ - final Subscriber child; - /** - * Holds an object that represents the current location in the buffer. - * Guarded by the emitter loop. - */ - Object index; - /** - * Keeps the sum of all requested amounts. - */ - final AtomicLong totalRequested; - /** Indicates an emission state. Guarded by this. */ - boolean emitting; - /** Indicates a missed update. Guarded by this. */ - boolean missed; - /** - * Indicates this child has been unsubscribed: the state is swapped in atomically and - * will prevent the dispatch() to emit (too many) values to a terminated child subscriber. - */ - static final long UNSUBSCRIBED = Long.MIN_VALUE; - - public InnerProducer(ReplaySubscriber parent, Subscriber child) { - this.parent = parent; - this.child = child; - this.totalRequested = new AtomicLong(); - } - - @Override - public void request(long n) { - // ignore negative requests - if (n < 0) { - return; - } - // In general, RxJava doesn't prevent concurrent requests (with each other or with - // an unsubscribe) so we need a CAS-loop, but we need to handle - // request overflow and unsubscribed/not requested state as well. - for (;;) { - // get the current request amount - long r = get(); - // if child called unsubscribe() do nothing - if (r == UNSUBSCRIBED) { - return; - } - // ignore zero requests except any first that sets in zero - if (r >= 0L && n == 0) { - return; - } - // otherwise, increase the request count - long u = r + n; - // and check for long overflow - if (u < 0) { - // cap at max value, which is essentially unlimited - u = Long.MAX_VALUE; - } - // try setting the new request value - if (compareAndSet(r, u)) { - // increment the total request counter - addTotalRequested(n); - // if successful, notify the parent dispacher this child can receive more - // elements - parent.manageRequests(); - - parent.buffer.replay(this); - return; - } - // otherwise, someone else changed the state (perhaps a concurrent - // request or unsubscription so retry - } - } - - /** - * Increments the total requested amount. - * @param n the additional request amount - */ - void addTotalRequested(long n) { - for (;;) { - long r = totalRequested.get(); - long u = r + n; - if (u < 0) { - u = Long.MAX_VALUE; - } - if (totalRequested.compareAndSet(r, u)) { - return; - } - } - } - - /** - * Indicate that values have been emitted to this child subscriber by the dispatch() method. - * @param n the number of items emitted - * @return the updated request value (may indicate how much can be produced or a terminal state) - */ - public long produced(long n) { - // we don't allow producing zero or less: it would be a bug in the operator - if (n <= 0) { - throw new IllegalArgumentException("Cant produce zero or less"); - } - for (;;) { - // get the current request value - long r = get(); - // if the child has unsubscribed, simply return and indicate this - if (r == UNSUBSCRIBED) { - return UNSUBSCRIBED; - } - // reduce the requested amount - long u = r - n; - // if the new amount is less than zero, we have a bug in this operator - if (u < 0) { - throw new IllegalStateException("More produced (" + n + ") than requested (" + r + ")"); - } - // try updating the request value - if (compareAndSet(r, u)) { - // and return the udpated value - return u; - } - // otherwise, some concurrent activity happened and we need to retry - } - } - - @Override - public boolean isUnsubscribed() { - return get() == UNSUBSCRIBED; - } - @Override - public void unsubscribe() { - long r = get(); - // let's see if we are unsubscribed - if (r != UNSUBSCRIBED) { - // if not, swap in the terminal state, this is idempotent - // because other methods using CAS won't overwrite this value, - // concurrent calls to unsubscribe will atomically swap in the same - // terminal value - r = getAndSet(UNSUBSCRIBED); - // and only one of them will see a non-terminated value before the swap - if (r != UNSUBSCRIBED) { - // remove this from the parent - parent.remove(this); - // After removal, we might have unblocked the other child subscribers: - // let's assume this child had 0 requested before the unsubscription while - // the others had non-zero. By removing this 'blocking' child, the others - // are now free to receive events - parent.manageRequests(); - } - } - } - /** - * Convenience method to auto-cast the index object. - * @return - */ - @SuppressWarnings("unchecked") - U index() { - return (U)index; - } - } /** - * The interface for interacting with various buffering logic. - * + * A subject that wraps another subject. * @param the value type */ - interface ReplayBuffer { - /** - * Adds a regular value to the buffer. - * @param value - */ - void next(T value); - /** - * Adds a terminal exception to the buffer - * @param e - */ - void error(Throwable e); - /** - * Adds a completion event to the buffer - */ - void complete(); - /** - * Tries to replay the buffered values to the - * subscriber inside the output if there - * is new value and requests available at the - * same time. - * @param output - */ - void replay(InnerProducer output); - } - - /** - * Holds an unbounded list of events. - * - * @param the value type - */ - static final class UnboundedReplayBuffer extends ArrayList implements ReplayBuffer { - /** */ - private static final long serialVersionUID = 7063189396499112664L; - final NotificationLite nl; - /** The total number of events in the buffer. */ - volatile int size; - - public UnboundedReplayBuffer(int capacityHint) { - super(capacityHint); - nl = NotificationLite.instance(); - } - @Override - public void next(T value) { - add(nl.next(value)); - size++; - } - - @Override - public void error(Throwable e) { - add(nl.error(e)); - size++; - } + public static final class SubjectWrapper extends Subject { + /** The wrapped subject. */ + final Subject subject; - @Override - public void complete() { - add(nl.completed()); - size++; + public SubjectWrapper(OnSubscribe func, Subject subject) { + super(func); + this.subject = subject; } @Override - public void replay(InnerProducer output) { - synchronized (output) { - if (output.emitting) { - output.missed = true; - return; - } - output.emitting = true; - } - for (;;) { - if (output.isUnsubscribed()) { - return; - } - int sourceIndex = size; - - Integer destIndexObject = output.index(); - int destIndex = destIndexObject != null ? destIndexObject.intValue() : 0; - - long r = output.get(); - long r0 = r; - long e = 0L; - - while (r != 0L && destIndex < sourceIndex) { - Object o = get(destIndex); - if (nl.accept(output.child, o)) { - return; - } - if (output.isUnsubscribed()) { - return; - } - destIndex++; - r--; - e++; - } - if (e != 0L) { - output.index = destIndex; - if (r0 != Long.MAX_VALUE) { - output.produced(e); - } - } - - synchronized (output) { - if (!output.missed) { - output.emitting = false; - return; - } - output.missed = false; - } - } - } - } - - /** - * Represents a node in a bounded replay buffer's linked list. - * - * @param the contained value type - */ - static final class Node extends AtomicReference { - /** */ - private static final long serialVersionUID = 245354315435971818L; - final Object value; - public Node(Object value) { - this.value = value; - } - } - - /** - * Base class for bounded buffering with options to specify an - * enter and leave transforms and custom truncation behavior. - * - * @param the value type - */ - static class BoundedReplayBuffer extends AtomicReference implements ReplayBuffer { - /** */ - private static final long serialVersionUID = 2346567790059478686L; - final NotificationLite nl; - - Node tail; - int size; - - public BoundedReplayBuffer() { - nl = NotificationLite.instance(); - Node n = new Node(null); - tail = n; - set(n); - } - - /** - * Add a new node to the linked list. - * @param n - */ - final void addLast(Node n) { - tail.set(n); - tail = n; - size++; - } - /** - * Remove the first node from the linked list. - */ - final void removeFirst() { - Node head = get(); - Node next = head.get(); - if (next == null) { - throw new IllegalStateException("Empty list!"); - } - size--; - // can't just move the head because it would retain the very first value - // can't null out the head's value because of late replayers would see null - setFirst(next.get()); - } - final void removeSome(int n) { - Node head = get(); - while (n > 0) { - head = head.get(); - n--; - size--; - } - - setFirst(head.get()); - } - /** - * Arranges the given node is the new head from now on. - * @param n - */ - final void setFirst(Node n) { - Node newHead = new Node(null); - newHead.lazySet(n); - if (n == null) { - tail = newHead; - } - set(newHead); - } - - @Override - public final void next(T value) { - Object o = enterTransform(nl.next(value)); - Node n = new Node(o); - addLast(n); - truncate(); + public void onNext(T args) { + subject.onNext(args); } @Override - public final void error(Throwable e) { - Object o = enterTransform(nl.error(e)); - Node n = new Node(o); - addLast(n); - truncateFinal(); + public void onError(Throwable e) { + subject.onError(e); } @Override - public final void complete() { - Object o = enterTransform(nl.completed()); - Node n = new Node(o); - addLast(n); - truncateFinal(); + public void onCompleted() { + subject.onCompleted(); } @Override - public final void replay(InnerProducer output) { - synchronized (output) { - if (output.emitting) { - output.missed = true; - return; - } - output.emitting = true; - } - for (;;) { - if (output.isUnsubscribed()) { - return; - } - - long r = output.get(); - long r0 = r; - long e = 0L; - - Node node = output.index(); - if (node == null) { - node = get(); - output.index = node; - } - - while (r != 0) { - Node v = node.get(); - if (v != null) { - Object o = leaveTransform(v.value); - if (nl.accept(output.child, o)) { - output.index = null; - return; - } - e++; - node = v; - } else { - break; - } - if (output.isUnsubscribed()) { - return; - } - } - - if (e != 0L) { - output.index = node; - if (r0 != Long.MAX_VALUE) { - output.produced(e); - } - } - - synchronized (output) { - if (!output.missed) { - output.emitting = false; - return; - } - output.missed = false; - } - } - - } - - /** - * Override this to wrap the NotificationLite object into a - * container to be used later by truncate. - * @param value - * @return - */ - Object enterTransform(Object value) { - return value; - } - /** - * Override this to unwrap the transformed value into a - * NotificationLite object. - * @param value - * @return - */ - Object leaveTransform(Object value) { - return value; - } - /** - * Override this method to truncate a non-terminated buffer - * based on its current properties. - */ - void truncate() { - - } - /** - * Override this method to truncate a terminated buffer - * based on its properties (i.e., truncate but the very last node). - */ - void truncateFinal() { - - } - /* test */ final void collect(Collection output) { - Node n = get(); - for (;;) { - Node next = n.get(); - if (next != null) { - Object o = next.value; - Object v = leaveTransform(o); - if (nl.isCompleted(v) || nl.isError(v)) { - break; - } - output.add(nl.getValue(v)); - n = next; - } else { - break; - } - } - } - /* test */ boolean hasError() { - return tail.value != null && nl.isError(leaveTransform(tail.value)); - } - /* test */ boolean hasCompleted() { - return tail.value != null && nl.isCompleted(leaveTransform(tail.value)); - } - } - - /** - * A bounded replay buffer implementation with size limit only. - * - * @param the value type - */ - static final class SizeBoundReplayBuffer extends BoundedReplayBuffer { - /** */ - private static final long serialVersionUID = -5898283885385201806L; - - final int limit; - public SizeBoundReplayBuffer(int limit) { - this.limit = limit; - } - - @Override - void truncate() { - // overflow can be at most one element - if (size > limit) { - removeFirst(); - } - } - - // no need for final truncation because values are truncated one by one - } - - /** - * Size and time bound replay buffer. - * - * @param the buffered value type - */ - static final class SizeAndTimeBoundReplayBuffer extends BoundedReplayBuffer { - /** */ - private static final long serialVersionUID = 3457957419649567404L; - final Scheduler scheduler; - final long maxAgeInMillis; - final int limit; - public SizeAndTimeBoundReplayBuffer(int limit, long maxAgeInMillis, Scheduler scheduler) { - this.scheduler = scheduler; - this.limit = limit; - this.maxAgeInMillis = maxAgeInMillis; - } - - @Override - Object enterTransform(Object value) { - return new Timestamped(scheduler.now(), value); - } - - @Override - Object leaveTransform(Object value) { - return ((Timestamped)value).getValue(); - } - - @Override - void truncate() { - long timeLimit = scheduler.now() - maxAgeInMillis; - - Node head = get(); - Node next = head.get(); - - int e = 0; - for (;;) { - if (next != null) { - if (size > limit) { - e++; - size--; - next = next.get(); - } else { - Timestamped v = (Timestamped)next.value; - if (v.getTimestampMillis() <= timeLimit) { - e++; - size--; - next = next.get(); - } else { - break; - } - } - } else { - break; - } - } - if (e != 0) { - setFirst(next); - } - } - @Override - void truncateFinal() { - long timeLimit = scheduler.now() - maxAgeInMillis; - - Node head = get(); - Node next = head.get(); - - int e = 0; - for (;;) { - if (next != null && size > 1) { - Timestamped v = (Timestamped)next.value; - if (v.getTimestampMillis() <= timeLimit) { - e++; - size--; - next = next.get(); - } else { - break; - } - } else { - break; - } - } - if (e != 0) { - setFirst(next); - } + public boolean hasObservers() { + return this.subject.hasObservers(); } } -} +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 5c31503da4..a5ff85864d 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -16,27 +16,33 @@ package rx.internal.operators; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.notNull; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; -import java.util.*; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.*; +import java.util.concurrent.atomic.AtomicInteger; -import org.junit.*; +import org.junit.Test; import org.mockito.InOrder; -import rx.*; -import rx.Scheduler.Worker; import rx.Observable; import rx.Observer; -import rx.functions.*; -import rx.internal.operators.OperatorReplay.BoundedReplayBuffer; -import rx.internal.operators.OperatorReplay.Node; -import rx.internal.operators.OperatorReplay.SizeAndTimeBoundReplayBuffer; +import rx.Scheduler; +import rx.Scheduler.Worker; +import rx.Subscription; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; import rx.observables.ConnectableObservable; -import rx.observers.TestSubscriber; -import rx.schedulers.*; +import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; public class OperatorReplayTest { @@ -733,132 +739,4 @@ public boolean isUnsubscribed() { } } - @Test - public void testBoundedReplayBuffer() { - BoundedReplayBuffer buf = new BoundedReplayBuffer(); - buf.addLast(new Node(1)); - buf.addLast(new Node(2)); - buf.addLast(new Node(3)); - buf.addLast(new Node(4)); - buf.addLast(new Node(5)); - - List values = new ArrayList(); - buf.collect(values); - - Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); - - buf.removeSome(2); - buf.removeFirst(); - buf.removeSome(2); - - values.clear(); - buf.collect(values); - Assert.assertTrue(values.isEmpty()); - - buf.addLast(new Node(5)); - buf.addLast(new Node(6)); - buf.collect(values); - - Assert.assertEquals(Arrays.asList(5, 6), values); - - } - - @Test - public void testTimedAndSizedTruncation() { - TestScheduler test = Schedulers.test(); - SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, test); - List values = new ArrayList(); - - buf.next(1); - test.advanceTimeBy(1, TimeUnit.SECONDS); - buf.next(2); - test.advanceTimeBy(1, TimeUnit.SECONDS); - buf.collect(values); - Assert.assertEquals(Arrays.asList(1, 2), values); - - buf.next(3); - buf.next(4); - values.clear(); - buf.collect(values); - Assert.assertEquals(Arrays.asList(3, 4), values); - - test.advanceTimeBy(2, TimeUnit.SECONDS); - buf.next(5); - - values.clear(); - buf.collect(values); - Assert.assertEquals(Arrays.asList(5), values); - - test.advanceTimeBy(2, TimeUnit.SECONDS); - buf.complete(); - - values.clear(); - buf.collect(values); - Assert.assertTrue(values.isEmpty()); - - Assert.assertEquals(1, buf.size); - Assert.assertTrue(buf.hasCompleted()); - } - - @Test - public void testBackpressure() { - final AtomicLong requested = new AtomicLong(); - Observable source = Observable.range(1, 1000) - .doOnRequest(new Action1() { - @Override - public void call(Long t) { - requested.addAndGet(t); - } - }); - ConnectableObservable co = source.replay(); - - TestSubscriber ts1 = TestSubscriber.create(10); - TestSubscriber ts2 = TestSubscriber.create(90); - - co.subscribe(ts1); - co.subscribe(ts2); - - ts2.requestMore(10); - - co.connect(); - - ts1.assertValueCount(10); - ts1.assertNoTerminalEvent(); - - ts2.assertValueCount(100); - ts2.assertNoTerminalEvent(); - - Assert.assertEquals(100, requested.get()); - } - - @Test - public void testBackpressureBounded() { - final AtomicLong requested = new AtomicLong(); - Observable source = Observable.range(1, 1000) - .doOnRequest(new Action1() { - @Override - public void call(Long t) { - requested.addAndGet(t); - } - }); - ConnectableObservable co = source.replay(50); - - TestSubscriber ts1 = TestSubscriber.create(10); - TestSubscriber ts2 = TestSubscriber.create(90); - - co.subscribe(ts1); - co.subscribe(ts2); - - ts2.requestMore(10); - - co.connect(); - - ts1.assertValueCount(10); - ts1.assertNoTerminalEvent(); - - ts2.assertValueCount(100); - ts2.assertNoTerminalEvent(); - - Assert.assertEquals(100, requested.get()); - } } \ No newline at end of file From 5411086ce6512ec24924b4528bc277fcbeb53dad Mon Sep 17 00:00:00 2001 From: George Campbell Date: Fri, 17 Jul 2015 16:33:59 -0700 Subject: [PATCH 334/857] Revert "If cache() now supports backpressure, correct javadocs to indicate this." This reverts commit ec3d522c826c3135b9f5e3a9bb34f62756ec95cc. --- src/main/java/rx/Observable.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 9e8b7db2d8..b0552417e2 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3583,7 +3583,8 @@ public final Observable> buffer(Observable boundary, int initialC * of items that will use up memory. *
*
Backpressure Support:
- *
This operator supports backpressure.
+ *
This operator does not support upstream backpressure as it is purposefully requesting and caching + * everything emitted.
*
Scheduler:
*
{@code cache} does not operate by default on a particular {@link Scheduler}.
*
@@ -3616,7 +3617,8 @@ public final Observable cache() { * of items that will use up memory. *
*
Backpressure Support:
- *
This operator supports backpressure.
+ *
This operator does not support upstream backpressure as it is purposefully requesting and caching + * everything emitted.
*
Scheduler:
*
{@code cache} does not operate by default on a particular {@link Scheduler}.
*
From e8dd4edf56d6c8b0db10ccb74a8690160bf245ef Mon Sep 17 00:00:00 2001 From: George Campbell Date: Fri, 17 Jul 2015 16:39:26 -0700 Subject: [PATCH 335/857] Revert "cache now supports backpressure" This reverts commit 18ff5afd380625f9157d9e9a3144baf845c09086. --- src/main/java/rx/Observable.java | 4 +- .../internal/operators/OnSubscribeCache.java | 76 +++ .../rx/internal/util/CachedObservable.java | 432 ------------------ .../rx/internal/util/LinkedArrayList.java | 136 ------ .../operators/OnSubscribeCacheTest.java | 164 +++++++ .../internal/util/CachedObservableTest.java | 264 ----------- .../rx/internal/util/LinkedArrayListTest.java | 37 -- 7 files changed, 242 insertions(+), 871 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OnSubscribeCache.java delete mode 100644 src/main/java/rx/internal/util/CachedObservable.java delete mode 100644 src/main/java/rx/internal/util/LinkedArrayList.java create mode 100644 src/test/java/rx/internal/operators/OnSubscribeCacheTest.java delete mode 100644 src/test/java/rx/internal/util/CachedObservableTest.java delete mode 100644 src/test/java/rx/internal/util/LinkedArrayListTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index b0552417e2..92b50f6081 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3594,7 +3594,7 @@ public final Observable> buffer(Observable boundary, int initialC * @see ReactiveX operators documentation: Replay */ public final Observable cache() { - return CachedObservable.from(this); + return create(new OnSubscribeCache(this)); } /** @@ -3629,7 +3629,7 @@ public final Observable cache() { * @see ReactiveX operators documentation: Replay */ public final Observable cache(int capacityHint) { - return CachedObservable.from(this, capacityHint); + return create(new OnSubscribeCache(this, capacityHint)); } /** diff --git a/src/main/java/rx/internal/operators/OnSubscribeCache.java b/src/main/java/rx/internal/operators/OnSubscribeCache.java new file mode 100644 index 0000000000..a568fd0e0b --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeCache.java @@ -0,0 +1,76 @@ +/** + * 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.atomic.AtomicIntegerFieldUpdater; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.subjects.ReplaySubject; +import rx.subjects.Subject; + +/** + * This method has similar behavior to {@link Observable#replay()} except that this auto-subscribes + * to the source Observable rather than returning a connectable Observable. + *

+ * + *

+ * This is useful with an Observable that you want to cache responses when you can't control the + * subscribe/unsubscribe behavior of all the Observers. + *

+ * Note: You sacrifice the ability to unsubscribe from the origin when you use this operator, so be + * careful not to use this operator on Observables that emit infinite or very large numbers of + * items, as this will use up memory. + * + * @param + * the cached value type + */ +public final class OnSubscribeCache implements OnSubscribe { + protected final Observable source; + protected final Subject cache; + volatile int sourceSubscribed; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater SRC_SUBSCRIBED_UPDATER + = AtomicIntegerFieldUpdater.newUpdater(OnSubscribeCache.class, "sourceSubscribed"); + + public OnSubscribeCache(Observable source) { + this(source, ReplaySubject. create()); + } + + public OnSubscribeCache(Observable source, int capacity) { + this(source, ReplaySubject. create(capacity)); + } + + /* accessible to tests */OnSubscribeCache(Observable source, Subject cache) { + this.source = source; + this.cache = cache; + } + + @Override + public void call(Subscriber s) { + if (SRC_SUBSCRIBED_UPDATER.compareAndSet(this, 0, 1)) { + source.subscribe(cache); + /* + * Note that we will never unsubscribe from 'source' unless we receive `onCompleted` or `onError`, + * as we want to receive and cache all of its values. + * + * This means this should never be used on an infinite or very large sequence, similar to toList(). + */ + } + cache.unsafeSubscribe(s); + } +} diff --git a/src/main/java/rx/internal/util/CachedObservable.java b/src/main/java/rx/internal/util/CachedObservable.java deleted file mode 100644 index cda4b9d277..0000000000 --- a/src/main/java/rx/internal/util/CachedObservable.java +++ /dev/null @@ -1,432 +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.util; - -import java.util.concurrent.atomic.*; - -import rx.*; -import rx.internal.operators.NotificationLite; -import rx.subscriptions.SerialSubscription; - -/** - * An observable which auto-connects to another observable, caches the elements - * from that observable but allows terminating the connection and completing the cache. - * - * @param the source element type - */ -public final class CachedObservable extends Observable { - /** The cache and replay state. */ - private CacheState state; - - /** - * Creates a cached Observable with a default capacity hint of 16. - * @param source the source Observable to cache - * @return the CachedObservable instance - */ - public static CachedObservable from(Observable source) { - return from(source, 16); - } - - /** - * Creates a cached Observable with the given capacity hint. - * @param source the source Observable to cache - * @param capacityHint the hint for the internal buffer size - * @return the CachedObservable instance - */ - public static CachedObservable from(Observable source, int capacityHint) { - if (capacityHint < 1) { - throw new IllegalArgumentException("capacityHint > 0 required"); - } - CacheState state = new CacheState(source, capacityHint); - CachedSubscribe onSubscribe = new CachedSubscribe(state); - return new CachedObservable(onSubscribe, state); - } - - /** - * Private constructor because state needs to be shared between the Observable body and - * the onSubscribe function. - * @param onSubscribe - * @param state - */ - private CachedObservable(OnSubscribe onSubscribe, CacheState state) { - super(onSubscribe); - this.state = state; - } - - /** - * Check if this cached observable is connected to its source. - * @return true if already connected - */ - /* public */boolean isConnected() { - return state.isConnected; - } - - /** - * Returns true if there are observers subscribed to this observable. - * @return - */ - /* public */ boolean hasObservers() { - return state.producers.length != 0; - } - - /** - * Returns the number of events currently cached. - * @return - */ - /* public */ int cachedEventCount() { - return state.size(); - } - - /** - * Contains the active child producers and the values to replay. - * - * @param - */ - static final class CacheState extends LinkedArrayList implements Observer { - /** The source observable to connect to. */ - final Observable source; - /** Holds onto the subscriber connected to source. */ - final SerialSubscription connection; - /** Guarded by connection (not this). */ - volatile ReplayProducer[] producers; - /** The default empty array of producers. */ - static final ReplayProducer[] EMPTY = new ReplayProducer[0]; - - final NotificationLite nl; - - /** Set to true after connection. */ - volatile boolean isConnected; - /** - * Indicates that the source has completed emitting values or the - * Observable was forcefully terminated. - */ - boolean sourceDone; - - public CacheState(Observable source, int capacityHint) { - super(capacityHint); - this.source = source; - this.producers = EMPTY; - this.nl = NotificationLite.instance(); - this.connection = new SerialSubscription(); - } - /** - * Adds a ReplayProducer to the producers array atomically. - * @param p - */ - public void addProducer(ReplayProducer p) { - // guarding by connection to save on allocating another object - // thus there are two distinct locks guarding the value-addition and child come-and-go - synchronized (connection) { - ReplayProducer[] a = producers; - int n = a.length; - ReplayProducer[] b = new ReplayProducer[n + 1]; - System.arraycopy(a, 0, b, 0, n); - b[n] = p; - producers = b; - } - } - /** - * Removes the ReplayProducer (if present) from the producers array atomically. - * @param p - */ - public void removeProducer(ReplayProducer p) { - synchronized (connection) { - ReplayProducer[] a = producers; - int n = a.length; - int j = -1; - for (int i = 0; i < n; i++) { - if (a[i].equals(p)) { - j = i; - break; - } - } - if (j < 0) { - return; - } - if (n == 1) { - producers = EMPTY; - return; - } - ReplayProducer[] b = new ReplayProducer[n - 1]; - System.arraycopy(a, 0, b, 0, j); - System.arraycopy(a, j + 1, b, j, n - j - 1); - producers = b; - } - } - /** - * Connects the cache to the source. - * Make sure this is called only once. - */ - public void connect() { - connection.set(source.subscribe(this)); - isConnected = true; - } - @Override - public void onNext(T t) { - Object o = nl.next(t); - synchronized (this) { - if (!sourceDone) { - add(o); - } else { - return; - } - } - dispatch(); - } - @Override - public void onError(Throwable e) { - Object o = nl.error(e); - synchronized (this) { - if (!sourceDone) { - sourceDone = true; - add(o); - } else { - return; - } - } - connection.unsubscribe(); - dispatch(); - } - @Override - public void onCompleted() { - Object o = nl.completed(); - synchronized (this) { - if (!sourceDone) { - sourceDone = true; - add(o); - } else { - return; - } - } - connection.unsubscribe(); - dispatch(); - } - /** - * Signals all known children there is work to do. - */ - void dispatch() { - ReplayProducer[] a = producers; - for (ReplayProducer rp : a) { - rp.replay(); - } - } - } - - /** - * Manages the subscription of child subscribers by setting up a replay producer and - * performs auto-connection of the very first subscription. - * @param the value type emitted - */ - static final class CachedSubscribe extends AtomicBoolean implements OnSubscribe { - /** */ - private static final long serialVersionUID = -2817751667698696782L; - final CacheState state; - public CachedSubscribe(CacheState state) { - this.state = state; - } - @Override - public void call(Subscriber t) { - // we can connect first because we replay everything anyway - ReplayProducer rp = new ReplayProducer(t, state); - state.addProducer(rp); - - t.add(rp); - t.setProducer(rp); - - // we ensure a single connection here to save an instance field of AtomicBoolean in state. - if (!get() && compareAndSet(false, true)) { - state.connect(); - } - - // no need to call rp.replay() here because the very first request will trigger it anyway - } - } - - /** - * Keeps track of the current request amount and the replay position for a child Subscriber. - * - * @param - */ - static final class ReplayProducer extends AtomicLong implements Producer, Subscription { - /** */ - private static final long serialVersionUID = -2557562030197141021L; - /** The actual child subscriber. */ - final Subscriber child; - /** The cache state object. */ - final CacheState state; - - /** - * Contains the reference to the buffer segment in replay. - * Accessed after reading state.size() and when emitting == true. - */ - Object[] currentBuffer; - /** - * Contains the index into the currentBuffer where the next value is expected. - * Accessed after reading state.size() and when emitting == true. - */ - int currentIndexInBuffer; - /** - * Contains the absolute index up until the values have been replayed so far. - */ - int index; - - /** Indicates there is a replay going on; guarded by this. */ - boolean emitting; - /** Indicates there were some state changes/replay attempts; guarded by this. */ - boolean missed; - - public ReplayProducer(Subscriber child, CacheState state) { - this.child = child; - this.state = state; - } - @Override - public void request(long n) { - for (;;) { - long r = get(); - if (r < 0) { - return; - } - long u = r + n; - if (u < 0) { - u = Long.MAX_VALUE; - } - if (compareAndSet(r, u)) { - replay(); - return; - } - } - } - /** - * Updates the request count to reflect values have been produced. - * @param n - * @return - */ - public long produced(long n) { - return addAndGet(-n); - } - - @Override - public boolean isUnsubscribed() { - return get() < 0; - } - @Override - public void unsubscribe() { - long r = get(); - if (r >= 0) { - r = getAndSet(-1L); // unsubscribed state is negative - if (r >= 0) { - state.removeProducer(this); - } - } - } - - /** - * Continue replaying available values if there are requests for them. - */ - public void replay() { - // make sure there is only a single thread emitting - synchronized (this) { - if (emitting) { - missed = true; - return; - } - emitting = true; - } - boolean skipFinal = false; - try { - final NotificationLite nl = state.nl; - final Subscriber child = this.child; - - for (;;) { - - long r = get(); - // read the size, if it is non-zero, we can safely read the head and - // read values up to the given absolute index - int s = state.size(); - if (s != 0) { - Object[] b = currentBuffer; - - // latch onto the very first buffer now that it is available. - if (b == null) { - b = state.head(); - currentBuffer = b; - } - final int n = b.length - 1; - int j = index; - int k = currentIndexInBuffer; - // eagerly emit any terminal event - if (r == 0) { - Object o = b[k]; - if (nl.isCompleted(o)) { - child.onCompleted(); - skipFinal = true; - unsubscribe(); - return; - } else - if (nl.isError(o)) { - child.onError(nl.getError(o)); - skipFinal = true; - unsubscribe(); - return; - } - } else - if (r > 0) { - int valuesProduced = 0; - - while (j < s && r > 0 && !child.isUnsubscribed()) { - if (k == n) { - b = (Object[])b[n]; - k = 0; - } - Object o = b[k]; - - if (nl.accept(child, o)) { - skipFinal = true; - unsubscribe(); - return; - } - - k++; - j++; - r--; - valuesProduced++; - } - - index = j; - currentIndexInBuffer = k; - currentBuffer = b; - produced(valuesProduced); - } - } - - synchronized (this) { - if (!missed) { - emitting = false; - skipFinal = true; - return; - } - missed = false; - } - } - } finally { - if (!skipFinal) { - synchronized (this) { - emitting = false; - } - } - } - } - } -} diff --git a/src/main/java/rx/internal/util/LinkedArrayList.java b/src/main/java/rx/internal/util/LinkedArrayList.java deleted file mode 100644 index 57a1289640..0000000000 --- a/src/main/java/rx/internal/util/LinkedArrayList.java +++ /dev/null @@ -1,136 +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.util; - -import java.util.*; - -/** - * A list implementation which combines an ArrayList with a LinkedList to - * avoid copying values when the capacity needs to be increased. - *

- * The class is non final to allow embedding it directly and thus saving on object allocation. - */ -public class LinkedArrayList { - /** The capacity of each array segment. */ - final int capacityHint; - /** - * Contains the head of the linked array list if not null. The - * length is always capacityHint + 1 and the last element is an Object[] pointing - * to the next element of the linked array list. - */ - Object[] head; - /** The tail array where new elements will be added. */ - Object[] tail; - /** - * The total size of the list; written after elements have been added (release) and - * and when read, the value indicates how many elements can be safely read (acquire). - */ - volatile int size; - /** The next available slot in the current tail. */ - int indexInTail; - /** - * Constructor with the capacity hint of each array segment. - * @param capacityHint - */ - public LinkedArrayList(int capacityHint) { - this.capacityHint = capacityHint; - } - /** - * Adds a new element to this list. - * @param o the object to add, nulls are accepted - */ - public void add(Object o) { - // if no value yet, create the first array - if (size == 0) { - head = new Object[capacityHint + 1]; - tail = head; - head[0] = o; - indexInTail = 1; - size = 1; - } else - // if the tail is full, create a new tail and link - if (indexInTail == capacityHint) { - Object[] t = new Object[capacityHint + 1]; - t[0] = o; - tail[capacityHint] = t; - tail = t; - indexInTail = 1; - size++; - } else { - tail[indexInTail] = o; - indexInTail++; - size++; - } - } - /** - * Returns the head buffer segment or null if the list is empty. - * @return - */ - public Object[] head() { - return head; - } - /** - * Returns the tail buffer segment or null if the list is empty. - * @return - */ - public Object[] tail() { - return tail; - } - /** - * Returns the total size of the list. - * @return - */ - public int size() { - return size; - } - /** - * Returns the index of the next slot in the tail buffer segment. - * @return - */ - public int indexInTail() { - return indexInTail; - } - /** - * Returns the capacity hint that indicates the capacity of each buffer segment. - * @return - */ - public int capacityHint() { - return capacityHint; - } - /* Test support */List toList() { - final int cap = capacityHint; - final int s = size; - final List list = new ArrayList(s + 1); - - Object[] h = head(); - int j = 0; - int k = 0; - while (j < s) { - list.add(h[k]); - j++; - if (++k == cap) { - k = 0; - h = (Object[])h[cap]; - } - } - - return list; - } - @Override - public String toString() { - return toList().toString(); - } -} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java new file mode 100644 index 0000000000..0d74cd878b --- /dev/null +++ b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java @@ -0,0 +1,164 @@ +/** + * 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.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 java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.subjects.AsyncSubject; +import rx.subjects.BehaviorSubject; +import rx.subjects.PublishSubject; +import rx.subjects.ReplaySubject; +import rx.subjects.Subject; + +public class OnSubscribeCacheTest { + + @Test + public void testCache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable o = Observable.create(new Observable.OnSubscribe() { + + @Override + public void call(final Subscriber observer) { + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + observer.onNext("one"); + observer.onCompleted(); + } + }).start(); + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + private void testWithCustomSubjectAndRepeat(Subject subject, Integer... expected) { + Observable source0 = Observable.just(1, 2, 3) + .subscribeOn(Schedulers.io()) + .flatMap(new Func1>() { + @Override + public Observable call(final Integer i) { + return Observable.timer(i * 20, TimeUnit.MILLISECONDS).map(new Func1() { + @Override + public Integer call(Long t1) { + return i; + } + }); + } + }); + + Observable source1 = Observable.create(new OnSubscribeCache(source0, subject)); + + Observable source2 = source1 + .repeat(4) + .zipWith(Observable.interval(0, 10, TimeUnit.MILLISECONDS, Schedulers.newThread()), new Func2() { + @Override + public Integer call(Integer t1, Long t2) { + return t1; + } + + }); + TestSubscriber ts = new TestSubscriber(); + source2.subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + System.out.println(ts.getOnNextEvents()); + ts.assertReceivedOnNext(Arrays.asList(expected)); + } + + @Test(timeout = 10000) + public void testWithAsyncSubjectAndRepeat() { + testWithCustomSubjectAndRepeat(AsyncSubject. create(), 3, 3, 3, 3); + } + + @Test(timeout = 10000) + public void testWithBehaviorSubjectAndRepeat() { + // BehaviorSubject just completes when repeated + testWithCustomSubjectAndRepeat(BehaviorSubject.create(0), 0, 1, 2, 3); + } + + @Test(timeout = 10000) + public void testWithPublishSubjectAndRepeat() { + // PublishSubject just completes when repeated + testWithCustomSubjectAndRepeat(PublishSubject. create(), 1, 2, 3); + } + + @Test + public void testWithReplaySubjectAndRepeat() { + testWithCustomSubjectAndRepeat(ReplaySubject. create(), 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3); + } + + @Test + public void testUnsubscribeSource() { + Action0 unsubscribe = mock(Action0.class); + Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, times(1)).call(); + } +} diff --git a/src/test/java/rx/internal/util/CachedObservableTest.java b/src/test/java/rx/internal/util/CachedObservableTest.java deleted file mode 100644 index c14018390f..0000000000 --- a/src/test/java/rx/internal/util/CachedObservableTest.java +++ /dev/null @@ -1,264 +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.util; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.*; - -import rx.*; -import rx.Observable.OnSubscribe; -import rx.Observable; -import rx.exceptions.TestException; -import rx.functions.*; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; - -public class CachedObservableTest { - @Test - public void testColdReplayNoBackpressure() { - CachedObservable source = CachedObservable.from(Observable.range(0, 1000)); - - assertFalse("Source is connected!", source.isConnected()); - - TestSubscriber ts = new TestSubscriber(); - - source.subscribe(ts); - - assertTrue("Source is not connected!", source.isConnected()); - assertFalse("Subscribers retained!", source.hasObservers()); - - ts.assertNoErrors(); - ts.assertTerminalEvent(); - List onNextEvents = ts.getOnNextEvents(); - assertEquals(1000, onNextEvents.size()); - - for (int i = 0; i < 1000; i++) { - assertEquals((Integer)i, onNextEvents.get(i)); - } - } - @Test - public void testColdReplayBackpressure() { - CachedObservable source = CachedObservable.from(Observable.range(0, 1000)); - - assertFalse("Source is connected!", source.isConnected()); - - TestSubscriber ts = new TestSubscriber(); - ts.requestMore(10); - - source.subscribe(ts); - - assertTrue("Source is not connected!", source.isConnected()); - assertTrue("Subscribers not retained!", source.hasObservers()); - - ts.assertNoErrors(); - assertTrue(ts.getOnCompletedEvents().isEmpty()); - List onNextEvents = ts.getOnNextEvents(); - assertEquals(10, onNextEvents.size()); - - for (int i = 0; i < 10; i++) { - assertEquals((Integer)i, onNextEvents.get(i)); - } - - ts.unsubscribe(); - assertFalse("Subscribers retained!", source.hasObservers()); - } - - @Test - public void testCache() throws InterruptedException { - final AtomicInteger counter = new AtomicInteger(); - Observable o = Observable.create(new Observable.OnSubscribe() { - - @Override - public void call(final Subscriber observer) { - new Thread(new Runnable() { - - @Override - public void run() { - counter.incrementAndGet(); - System.out.println("published observable being executed"); - observer.onNext("one"); - observer.onCompleted(); - } - }).start(); - } - }).cache(); - - // we then expect the following 2 subscriptions to get that same value - final CountDownLatch latch = new CountDownLatch(2); - - // subscribe once - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - // subscribe again - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - if (!latch.await(1000, TimeUnit.MILLISECONDS)) { - fail("subscriptions did not receive values"); - } - assertEquals(1, counter.get()); - } - - @Test - public void testUnsubscribeSource() { - Action0 unsubscribe = mock(Action0.class); - Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); - o.subscribe(); - o.subscribe(); - o.subscribe(); - verify(unsubscribe, times(1)).call(); - } - - @Test - public void testTake() { - TestSubscriber ts = new TestSubscriber(); - - CachedObservable cached = CachedObservable.from(Observable.range(1, 100)); - cached.take(10).subscribe(ts); - - ts.assertNoErrors(); - ts.assertTerminalEvent(); - ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - ts.assertUnsubscribed(); - assertFalse(cached.hasObservers()); - } - - @Test - public void testAsync() { - Observable source = Observable.range(1, 10000); - for (int i = 0; i < 100; i++) { - TestSubscriber ts1 = new TestSubscriber(); - - CachedObservable cached = CachedObservable.from(source); - - cached.observeOn(Schedulers.computation()).subscribe(ts1); - - ts1.awaitTerminalEvent(2, TimeUnit.SECONDS); - ts1.assertNoErrors(); - ts1.assertTerminalEvent(); - assertEquals(10000, ts1.getOnNextEvents().size()); - - TestSubscriber ts2 = new TestSubscriber(); - cached.observeOn(Schedulers.computation()).subscribe(ts2); - - ts2.awaitTerminalEvent(2, TimeUnit.SECONDS); - ts2.assertNoErrors(); - ts2.assertTerminalEvent(); - assertEquals(10000, ts2.getOnNextEvents().size()); - } - } - @Test - public void testAsyncComeAndGo() { - Observable source = Observable.timer(1, 1, TimeUnit.MILLISECONDS) - .take(1000) - .subscribeOn(Schedulers.io()); - CachedObservable cached = CachedObservable.from(source); - - Observable output = cached.observeOn(Schedulers.computation()); - - List> list = new ArrayList>(100); - for (int i = 0; i < 100; i++) { - TestSubscriber ts = new TestSubscriber(); - list.add(ts); - output.skip(i * 10).take(10).subscribe(ts); - } - - List expected = new ArrayList(); - for (int i = 0; i < 10; i++) { - expected.add((long)(i - 10)); - } - int j = 0; - for (TestSubscriber ts : list) { - ts.awaitTerminalEvent(3, TimeUnit.SECONDS); - ts.assertNoErrors(); - ts.assertTerminalEvent(); - - for (int i = j * 10; i < j * 10 + 10; i++) { - expected.set(i - j * 10, (long)i); - } - - ts.assertReceivedOnNext(expected); - - j++; - } - } - - @Test - public void testNoMissingBackpressureException() { - final int m = 4 * 1000 * 1000; - Observable firehose = Observable.create(new OnSubscribe() { - @Override - public void call(Subscriber t) { - for (int i = 0; i < m; i++) { - t.onNext(i); - } - t.onCompleted(); - } - }); - - TestSubscriber ts = new TestSubscriber(); - firehose.cache().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); - - ts.awaitTerminalEvent(3, TimeUnit.SECONDS); - ts.assertNoErrors(); - ts.assertTerminalEvent(); - - assertEquals(100, ts.getOnNextEvents().size()); - } - - @Test - public void testValuesAndThenError() { - Observable source = Observable.range(1, 10) - .concatWith(Observable.error(new TestException())) - .cache(); - - - TestSubscriber ts = new TestSubscriber(); - source.subscribe(ts); - - ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); - Assert.assertEquals(1, ts.getOnErrorEvents().size()); - - TestSubscriber ts2 = new TestSubscriber(); - source.subscribe(ts2); - - ts2.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - Assert.assertTrue(ts2.getOnCompletedEvents().isEmpty()); - Assert.assertEquals(1, ts2.getOnErrorEvents().size()); - } -} diff --git a/src/test/java/rx/internal/util/LinkedArrayListTest.java b/src/test/java/rx/internal/util/LinkedArrayListTest.java deleted file mode 100644 index af7e167c19..0000000000 --- a/src/test/java/rx/internal/util/LinkedArrayListTest.java +++ /dev/null @@ -1,37 +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.util; - -import java.util.*; -import static org.junit.Assert.*; - -import org.junit.Test; - -public class LinkedArrayListTest { - @Test - public void testAdd() { - LinkedArrayList list = new LinkedArrayList(16); - - List expected = new ArrayList(32); - for (int i = 0; i < 32; i++) { - list.add(i); - expected.add(i); - } - - assertEquals(expected, list.toList()); - assertEquals(32, list.size()); - } -} From d43b3d1f76bc13cd4c2bf8cb4f951f751aea86e6 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 20 Jul 2015 19:04:15 +0200 Subject: [PATCH 336/857] Fix autoConnect calling onStart twice. --- .../java/rx/internal/operators/OnSubscribeAutoConnect.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java b/src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java index c664717332..75ea9c82cf 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java +++ b/src/main/java/rx/internal/operators/OnSubscribeAutoConnect.java @@ -18,9 +18,11 @@ import java.util.concurrent.atomic.AtomicInteger; import rx.Observable.OnSubscribe; -import rx.*; +import rx.Subscriber; +import rx.Subscription; import rx.functions.Action1; import rx.observables.ConnectableObservable; +import rx.observers.Subscribers; /** * Wraps a ConnectableObservable and calls its connect() method once @@ -47,7 +49,7 @@ public OnSubscribeAutoConnect(ConnectableObservable source, } @Override public void call(Subscriber child) { - source.unsafeSubscribe(child); + source.unsafeSubscribe(Subscribers.wrap(child)); if (clients.incrementAndGet() == numberOfSubscribers) { source.connect(connection); } From 1e2a7252b308dd3ce2fea78eaf2134ea055b8565 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 20 Jul 2015 10:03:30 -0700 Subject: [PATCH 337/857] Private toObservable renamed to asObservable - Making room for the public toObservable method. --- src/main/java/rx/Single.java | 56 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 2aad23fb9a..c1dd59bcb8 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -244,7 +244,7 @@ public static interface Transformer extends Func1, Single> { * * @warn more complete description needed */ - private static Observable toObservable(Single t) { + private static Observable asObservable(Single t) { // is this sufficient, or do I need to keep the outer Single and subscribe to it? return Observable.create(t.onSubscribe); } @@ -265,7 +265,7 @@ private static Observable toObservable(Single t) { * @see ReactiveX operators documentation: To */ private final Single> nest() { - return Single.just(toObservable(this)); + return Single.just(asObservable(this)); } /* ********************************************************************************************************* @@ -290,7 +290,7 @@ private final Single> nest() { * @see ReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2) { - return Observable.concat(toObservable(t1), toObservable(t2)); + return Observable.concat(asObservable(t1), asObservable(t2)); } /** @@ -312,7 +312,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3) { - return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3)); + return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3)); } /** @@ -336,7 +336,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4) { - return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4)); + return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4)); } /** @@ -362,7 +362,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5) { - return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5)); + return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5)); } /** @@ -390,7 +390,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { - return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6)); + return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6)); } /** @@ -420,7 +420,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { - return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7)); + return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7)); } /** @@ -452,7 +452,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { - return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8)); + return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8)); } /** @@ -486,7 +486,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { - return Observable.concat(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8), toObservable(t9)); + return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8), asObservable(t9)); } /** @@ -694,7 +694,7 @@ public void onError(Throwable error) { * @see ReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2) { - return Observable.merge(toObservable(t1), toObservable(t2)); + return Observable.merge(asObservable(t1), asObservable(t2)); } /** @@ -719,7 +719,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3) { - return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3)); + return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3)); } /** @@ -746,7 +746,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4) { - return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4)); + return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4)); } /** @@ -775,7 +775,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5) { - return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5)); + return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5)); } /** @@ -806,7 +806,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { - return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6)); + return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6)); } /** @@ -839,7 +839,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { - return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7)); + return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7)); } /** @@ -874,7 +874,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { - return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8)); + return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8)); } /** @@ -911,7 +911,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { - return Observable.merge(toObservable(t1), toObservable(t2), toObservable(t3), toObservable(t4), toObservable(t5), toObservable(t6), toObservable(t7), toObservable(t8), toObservable(t9)); + return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8), asObservable(t9)); } /** @@ -935,7 +935,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, final Func2 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2) }).lift(new OperatorZip(zipFunction)); } /** @@ -961,7 +961,7 @@ public final static Single zip(Single o1, SingleReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Func3 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3) }).lift(new OperatorZip(zipFunction)); } /** @@ -989,7 +989,7 @@ public final static Single zip(Single o1, Singl * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Func4 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4) }).lift(new OperatorZip(zipFunction)); } /** @@ -1019,7 +1019,7 @@ public final static Single zip(Single o1, S * @see ReactiveX operators documentation: Zip */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Func5 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5) }).lift(new OperatorZip(zipFunction)); } /** @@ -1052,7 +1052,7 @@ public final static Single zip(Single o */ public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Func6 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6) }).lift(new OperatorZip(zipFunction)); } /** @@ -1087,7 +1087,7 @@ public final static Single zip(Single Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Func7 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6), toObservable(o7) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6), asObservable(o7) }).lift(new OperatorZip(zipFunction)); } /** @@ -1124,7 +1124,7 @@ public final static Single zip(Single Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, Func8 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6), toObservable(o7), toObservable(o8) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6), asObservable(o7), asObservable(o8) }).lift(new OperatorZip(zipFunction)); } /** @@ -1163,7 +1163,7 @@ public final static Single zip(Single Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, Single o9, Func9 zipFunction) { - return just(new Observable[] { toObservable(o1), toObservable(o2), toObservable(o3), toObservable(o4), toObservable(o5), toObservable(o6), toObservable(o7), toObservable(o8), toObservable(o9) }).lift(new OperatorZip(zipFunction)); + return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6), asObservable(o7), asObservable(o8), asObservable(o9) }).lift(new OperatorZip(zipFunction)); } /** @@ -1222,7 +1222,7 @@ public final Single flatMap(final Func1ReactiveX operators documentation: FlatMap */ public final Observable flatMapObservable(Func1> func) { - return Observable.merge(toObservable(map(func))); + return Observable.merge(asObservable(map(func))); } /** @@ -1748,7 +1748,7 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single error(new TimeoutException()); } - return lift(new OperatorTimeout(timeout, timeUnit, toObservable(other), scheduler)); + return lift(new OperatorTimeout(timeout, timeUnit, asObservable(other), scheduler)); } /** From 6870e7ee20911dca1de2af1db37c7c9e4dd84a1c Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 20 Jul 2015 10:08:54 -0700 Subject: [PATCH 338/857] Single.toObservable --- src/main/java/rx/Single.java | 11 +++++++++++ src/test/java/rx/SingleTest.java | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index c1dd59bcb8..ede27c8eb6 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1649,6 +1649,17 @@ public void onNext(T t) { public final Single subscribeOn(Scheduler scheduler) { return nest().lift(new OperatorSubscribeOn(scheduler)); } + + /** + * Converts this Single into an {@link Observable}. + *

+ * + * + * @return an {@link Observable} that emits a single item T. + */ + public final Observable toObservable() { + return asObservable(this); + } /** * Returns a Single that mirrors the source Single but applies a timeout policy for its emitted item. If it diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 778feffb3c..1efd1ae5a7 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -452,4 +452,13 @@ public void onStart() { ts.assertValue("hello"); } + + @Test + public void testToObservable() { + Observable a = Single.just("a").toObservable(); + TestSubscriber ts = TestSubscriber.create(); + a.subscribe(ts); + ts.assertValue("a"); + ts.assertCompleted(); + } } From 14f82de73fa2aad3311462f72c8075454fd2d977 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 20 Jul 2015 10:58:51 -0700 Subject: [PATCH 339/857] 1.0.13 --- CHANGES.md | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index a92a299b5e..3f6776cd46 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,132 @@ # RxJava Releases # +### Version 1.0.13 – July 20th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.13%7C)) ### + +This release has quite a few bug fixes and some new functionality. Items of note are detailed here with the list of changes at the bottom. + +##### merge + +The `merge` operator went through a major rewrite to fix some edge case bugs in the previous version. This has been sitting for months going through review and performance testing due to the importance and ubiquity of its usage. It is believed this rewrite is now production ready and achieves the goal of being more correct (no known edge cases at this time) while retaining comparable performance and memory usage. + +Special thanks to @akarnokd for this as `merge` is a challenging one to implement. + +##### window fix and behavior change + +Unsubscription bugs were fixed in `window`. Along the way it also resulted in a fix to one of the `window` overloads that had a functional discrepancy. + +```java +window(Func0> closingSelector) +``` + +This is a small behavior change that corrects it. If you use this overload, please review the change to ensure your application is not affected by an assumption of the previously buggy behavior: https://github.com/ReactiveX/RxJava/pull/3039 + +Note that this behavior change only affects that particular overload while the broader bug fixes affect all `window` overloads. + +##### rx.Single + +After [much discussion](https://github.com/ReactiveX/RxJava/issues/1594) it was decided to add a new type to represent an `Observable` that emits a single item. Much bike-shedding led to the name `Single`. This was chosen because `Future`, `Promise` and `Task` are overused and already have nuanced connotations that differ from `rx.Single`, and we didn't want long, obnoxious names with `Observable` as a prefix or suffix. Read the issue thread if you want to dig into the long debates. + +If you want to understand the reasoning behind adding this type, you can read about it [in this comment](https://github.com/ReactiveX/RxJava/issues/1594#issuecomment-101300655). + +In short, request/response semantics are so common that it was decided worth creating a type that composes well with an `Observable` but only exposes request/response. The difference in behavior and comparability was also deemed worth having an alternative to `Future`. In particular, a `Single` is lazy whereas `Future` is eager. Additionally, merging of `Single`s becomes an `Observable`, whereas combining `Future`s always emits another `Future`. + +Note that the API is added in an `@Experimental` state. We are fairly confident this will stick around, but are holding final judgement until it is used more broadly. We will promote to a stable API in v1.1 or v1.2. + +Examples below demonstrate use of `Single`. + +```java +// Hello World +Single hello = Single.just("Hello World!"); +hello.subscribe(System.out::println); + +// Async request/response +Single one = getData(1); +Single two = getOtherData(2); + +// merge request/responses into an Observable of multiple values (not possible with Futures) +Observable merged = one.mergeWith(two); + +// zip request/responses into another Single (similar to combining 2 Futures) +Single zipped = one.zipWith(two, (a, b) -> a + b); + +// flatMap to a Single +Single flatMapSingle = one.flatMap(v -> { + return getOtherData(5); +}); + +// flatMap to an Observable +Observable flatMapObservable = one.flatMapObservable(v -> { + return Observable.just(1, 2, 3); +}); + +// toObservable +Observable toObservable = one.toObservable(); + +// toSingle +Single toSingle = Observable.just(1).toSingle(); + +public static Single getData(int id) { + return Single. create(s -> { + // do blocking IO + s.onSuccess("data_" + id); + }).subscribeOn(Schedulers.io()); +} + +public static Single getOtherData(int id) { + return Single. create(s -> { + // simulate non-blocking IO + new Thread(() -> { + try { + s.onSuccess("other_" + id); + } catch (Exception e) { + s.onError(e); + } + }).start(); + }); +} +``` + +##### ConnectableObservable.autoConnect + +A new feature was added to `ConnectableObservable` similar in behavior to `refCount()`, except that it doesn't disconnect when subscribers are lost. This is useful in triggering an "auto connect" once a certain number of subscribers have subscribed. + +The [JavaDocs](https://github.com/ReactiveX/RxJava/blob/1877fa7bbc176029bcb5af00d8a7715dfbb6d373/src/main/java/rx/observables/ConnectableObservable.java#L96) and [unit tests](https://github.com/ReactiveX/RxJava/blob/1.x/src/test/java/rx/observables/ConnectableObservableTest.java) are good places to understand the feature. + +##### Deprecated onBackpressureBlock + +The `onBackpressureBlock` operator has been deprecated. It will not ever be removed during the 1.x lifecycle, but it is recommended to not use it. It has proven to be a common source of deadlocks and is difficult to debug. It is instead recommended to use non-blocking approaches to backpressure, rather than callstack blocking. Approaches to backpressure and flow control are [discussed on the wiki](https://github.com/ReactiveX/RxJava/wiki/Backpressure). + +#### Changes + +* [Pull 3012] (https://github.com/ReactiveX/RxJava/pull/3012) rx.Single +* [Pull 2983] (https://github.com/ReactiveX/RxJava/pull/2983) Fixed multiple calls to onStart. +* [Pull 2970] (https://github.com/ReactiveX/RxJava/pull/2970) Deprecated onBackpressureBlock +* [Pull 2997] (https://github.com/ReactiveX/RxJava/pull/2997) Fix retry() race conditions +* [Pull 3028] (https://github.com/ReactiveX/RxJava/pull/3028) Delay: error cut ahead was not properly serialized +* [Pull 3042] (https://github.com/ReactiveX/RxJava/pull/3042) add backpressure support for defaultIfEmpty() +* [Pull 3049] (https://github.com/ReactiveX/RxJava/pull/3049) single: add toSingle method to Observable +* [Pull 3055] (https://github.com/ReactiveX/RxJava/pull/3055) toSingle() should use unsafeSubscribe +* [Pull 3023] (https://github.com/ReactiveX/RxJava/pull/3023) ConnectableObservable autoConnect operator +* [Pull 2928] (https://github.com/ReactiveX/RxJava/pull/2928) Merge and MergeMaxConcurrent unified and rewritten +* [Pull 3039] (https://github.com/ReactiveX/RxJava/pull/3039) Window with Observable: fixed unsubscription and behavior +* [Pull 3045] (https://github.com/ReactiveX/RxJava/pull/3045) ElementAt request management enhanced +* [Pull 3048] (https://github.com/ReactiveX/RxJava/pull/3048) CompositeException extra NPE protection +* [Pull 3052] (https://github.com/ReactiveX/RxJava/pull/3052) Reduce test failure likelihood of testMultiThreadedWithNPEinMiddle +* [Pull 3031] (https://github.com/ReactiveX/RxJava/pull/3031) Fix OperatorFlatMapPerf.flatMapIntPassthruAsync Perf Test +* [Pull 2975] (https://github.com/ReactiveX/RxJava/pull/2975) Deprecate and rename two timer overloads to interval +* [Pull 2982] (https://github.com/ReactiveX/RxJava/pull/2982) TestSubscriber - add factory methods +* [Pull 2995] (https://github.com/ReactiveX/RxJava/pull/2995) switchOnNext - ensure initial requests additive and fix request overflow +* [Pull 2972] (https://github.com/ReactiveX/RxJava/pull/2972) Fixed window(time) to work properly with unsubscription, added +* [Pull 2990] (https://github.com/ReactiveX/RxJava/pull/2990) Improve Subscriber readability +* [Pull 3018] (https://github.com/ReactiveX/RxJava/pull/3018) TestSubscriber - fix awaitTerminalEventAndUnsubscribeOnTimeout +* [Pull 3034] (https://github.com/ReactiveX/RxJava/pull/3034) Instantiate EMPTY lazily +* [Pull 3033] (https://github.com/ReactiveX/RxJava/pull/3033) takeLast() javadoc fixes, standardize parameter names (count instead of num) +* [Pull 3043] (https://github.com/ReactiveX/RxJava/pull/3043) TestSubscriber javadoc cleanup +* [Pull 3065] (https://github.com/ReactiveX/RxJava/pull/3065) add Subscribers.wrap +* [Pull 3091] (https://github.com/ReactiveX/RxJava/pull/3091) Fix autoConnect calling onStart twice. +* [Pull 3092] (https://github.com/ReactiveX/RxJava/pull/3092) Single.toObservable + + ### Version 1.0.12 – June 9th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.12%7C)) ### * [Pull 2963] (https://github.com/ReactiveX/RxJava/pull/2963) Set of standard producers and updated queue implementations From 0a24f8d01e9707109b24cc1d9a8123028ade6e60 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 20 Jul 2015 23:04:52 +0200 Subject: [PATCH 340/857] Fix request != 0 checking in the scalar paths of merge() --- .../rx/internal/operators/OperatorMerge.java | 6 +++-- src/test/java/rx/BackpressureTests.java | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 98cb548391..d2f52cb204 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -315,7 +315,8 @@ void tryEmit(InnerSubscriber subscriber, T value) { if (r != 0L) { synchronized (this) { // if nobody is emitting and child has available requests - if (!emitting) { + r = producer.get(); + if (!emitting && r != 0L) { emitting = true; success = true; } @@ -422,7 +423,8 @@ void tryEmit(T value) { if (r != 0L) { synchronized (this) { // if nobody is emitting and child has available requests - if (!emitting) { + r = producer.get(); + if (!emitting && r != 0L) { emitting = true; success = true; } diff --git a/src/test/java/rx/BackpressureTests.java b/src/test/java/rx/BackpressureTests.java index ffa2e01129..439b18a08f 100644 --- a/src/test/java/rx/BackpressureTests.java +++ b/src/test/java/rx/BackpressureTests.java @@ -123,6 +123,30 @@ public void testMergeAsync() { assertTrue(c2.get() < RxRingBuffer.SIZE * 5); } + @Test + public void testMergeAsyncThenObserveOnLoop() { + for (int i = 0; i < 500; i++) { + if (i % 10 == 0) { + System.out.println("testMergeAsyncThenObserveOnLoop >> " + i); + } + // Verify there is no MissingBackpressureException + int NUM = (int) (RxRingBuffer.SIZE * 4.1); + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + + TestSubscriber ts = new TestSubscriber(); + Observable merged = Observable.merge( + incrementingIntegers(c1).subscribeOn(Schedulers.computation()), + incrementingIntegers(c2).subscribeOn(Schedulers.computation())); + + merged.observeOn(Schedulers.io()).take(NUM).subscribe(ts); + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + System.out.println("testMergeAsyncThenObserveOn => Received: " + ts.getOnNextEvents().size() + " Emitted: " + c1.get() + " / " + c2.get()); + assertEquals(NUM, ts.getOnNextEvents().size()); + } + } + @Test public void testMergeAsyncThenObserveOn() { int NUM = (int) (RxRingBuffer.SIZE * 4.1); From 18a47ea64f2510c4685427a1a64a991028048447 Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 21 Jul 2015 12:43:36 -0700 Subject: [PATCH 341/857] window() behavior changed, so did marble diagram & thus its size --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 5aaf37c479..dd83aaf5e0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -9152,7 +9152,7 @@ public final Observable withLatestFrom(Observable other, * Observable emits connected, non-overlapping windows. It emits the current window and opens a new one * whenever the Observable produced by the specified {@code closingSelector} emits an item. *

- * + * *

*
Backpressure Support:
*
This operator does not support backpressure as it uses the {@code closingSelector} to control data From 5430c98db10f29680da0a848c56ae3da5ac0cabc Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 24 Jul 2015 15:13:19 +1000 Subject: [PATCH 342/857] fix SynchronizedQueue.equals --- .../rx/internal/util/SynchronizedQueue.java | 20 +++++++++++++++---- .../internal/util/SynchronizedQueueTest.java | 15 ++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/test/java/rx/internal/util/SynchronizedQueueTest.java diff --git a/src/main/java/rx/internal/util/SynchronizedQueue.java b/src/main/java/rx/internal/util/SynchronizedQueue.java index 9fe867d93a..8f0c4a7372 100644 --- a/src/main/java/rx/internal/util/SynchronizedQueue.java +++ b/src/main/java/rx/internal/util/SynchronizedQueue.java @@ -99,13 +99,25 @@ public synchronized String toString() { } @Override - public synchronized boolean equals(Object o) { - return list.equals(o); + public int hashCode() { + return list.hashCode(); } @Override - public synchronized int hashCode() { - return list.hashCode(); + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SynchronizedQueue other = (SynchronizedQueue) obj; + if (list == null) { + if (other.list != null) + return false; + } else if (!list.equals(other.list)) + return false; + return true; } @Override diff --git a/src/test/java/rx/internal/util/SynchronizedQueueTest.java b/src/test/java/rx/internal/util/SynchronizedQueueTest.java new file mode 100644 index 0000000000..98779ea1b9 --- /dev/null +++ b/src/test/java/rx/internal/util/SynchronizedQueueTest.java @@ -0,0 +1,15 @@ +package rx.internal.util; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class SynchronizedQueueTest { + + @Test + public void testEquals() { + SynchronizedQueue q = new SynchronizedQueue(); + assertTrue(q.equals(q)); + } + +} From 07eefcd001f7ec6b9b8557b9843f4506b8a4e9ec Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 24 Jul 2015 08:54:37 +0200 Subject: [PATCH 343/857] Fix take swallowing exception if thrown by the exactly the nth onNext call to it. --- .../rx/internal/operators/OperatorTake.java | 26 ++++++---- .../internal/operators/OperatorTakeTest.java | 49 ++++++++++--------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorTake.java b/src/main/java/rx/internal/operators/OperatorTake.java index 0cc42b88ef..31811537b5 100644 --- a/src/main/java/rx/internal/operators/OperatorTake.java +++ b/src/main/java/rx/internal/operators/OperatorTake.java @@ -43,12 +43,13 @@ public OperatorTake(int limit) { public Subscriber call(final Subscriber child) { final Subscriber parent = new Subscriber() { - int count = 0; - boolean completed = false; + int count; + boolean completed; @Override public void onCompleted() { if (!completed) { + completed = true; child.onCompleted(); } } @@ -56,20 +57,27 @@ public void onCompleted() { @Override public void onError(Throwable e) { if (!completed) { - child.onError(e); + completed = true; + try { + child.onError(e); + } finally { + unsubscribe(); + } } } @Override public void onNext(T i) { if (!isUnsubscribed()) { - if (++count >= limit) { - completed = true; - } + boolean stop = ++count >= limit; child.onNext(i); - if (completed) { - child.onCompleted(); - unsubscribe(); + if (stop && !completed) { + completed = true; + try { + child.onCompleted(); + } finally { + unsubscribe(); + } } } } diff --git a/src/test/java/rx/internal/operators/OperatorTakeTest.java b/src/test/java/rx/internal/operators/OperatorTakeTest.java index 111eb6abbd..3384445d5b 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeTest.java @@ -16,36 +16,21 @@ package rx.internal.operators; import static org.junit.Assert.*; -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 static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; import java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; import org.junit.Test; import org.mockito.InOrder; -import rx.Observable; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Producer; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.observers.Subscribers; -import rx.observers.TestSubscriber; +import rx.exceptions.TestException; +import rx.functions.*; +import rx.observers.*; import rx.schedulers.Schedulers; public class OperatorTakeTest { @@ -414,4 +399,22 @@ public void call(Long n) { ts.assertNoErrors(); assertEquals(2,requests.get()); } + + @Test + public void takeFinalValueThrows() { + Observable source = Observable.just(1).take(1); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } } From ac8b504c19b0f3a3d30f1031ff03469e16496fc2 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 24 Jul 2015 18:05:49 +1000 Subject: [PATCH 344/857] remove OperatorOnErrorFlatMap because unused --- .../operators/OperatorOnErrorFlatMap.java | 84 ------------------- 1 file changed, 84 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OperatorOnErrorFlatMap.java diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorFlatMap.java b/src/main/java/rx/internal/operators/OperatorOnErrorFlatMap.java deleted file mode 100644 index f8e56971f8..0000000000 --- a/src/main/java/rx/internal/operators/OperatorOnErrorFlatMap.java +++ /dev/null @@ -1,84 +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 rx.Observable; -import rx.Observable.Operator; -import rx.Subscriber; -import rx.exceptions.OnErrorThrowable; -import rx.functions.Func1; -import rx.plugins.RxJavaPlugins; - -/** - * Allows inserting onNext events into a stream when onError events are received - * and continuing the original sequence instead of terminating. Thus it allows a sequence - * with multiple onError events. - */ -public final class OperatorOnErrorFlatMap implements Operator { - - private final Func1> resumeFunction; - - public OperatorOnErrorFlatMap(Func1> f) { - this.resumeFunction = f; - } - - @Override - public Subscriber call(final Subscriber child) { - return new Subscriber(child) { - - @Override - public void onCompleted() { - child.onCompleted(); - } - - @Override - public void onError(Throwable e) { - try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); - Observable resume = resumeFunction.call(OnErrorThrowable.from(e)); - resume.unsafeSubscribe(new Subscriber() { - - @Override - public void onCompleted() { - // ignore as we will continue the parent Observable - } - - @Override - public void onError(Throwable e) { - // if the splice also fails we shut it all down - child.onError(e); - } - - @Override - public void onNext(T t) { - child.onNext(t); - } - - }); - } catch (Throwable e2) { - child.onError(e2); - } - } - - @Override - public void onNext(T t) { - child.onNext(t); - } - - }; - } - -} From 66519c0ccb23a750874fd2194fd828fcc59f893a Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 24 Jul 2015 11:12:32 +0200 Subject: [PATCH 345/857] Unit tests and cleanup of JCTools' queues. --- .../util/atomic/MpscLinkedAtomicQueue.java | 2 +- .../util/atomic/SpscLinkedAtomicQueue.java | 2 +- .../internal/util/unsafe/MpmcArrayQueue.java | 20 +- .../internal/util/unsafe/MpscLinkedQueue.java | 2 +- .../internal/util/unsafe/SpmcArrayQueue.java | 20 +- .../internal/util/unsafe/SpscArrayQueue.java | 23 +- .../internal/util/unsafe/SpscLinkedQueue.java | 2 +- .../rx/internal/util/JCToolsQueueTests.java | 419 +++++++++++++++++- 8 files changed, 428 insertions(+), 62 deletions(-) diff --git a/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java index ebc1264599..261e4c2a1b 100644 --- a/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java +++ b/src/main/java/rx/internal/util/atomic/MpscLinkedAtomicQueue.java @@ -58,7 +58,7 @@ public MpscLinkedAtomicQueue() { @Override public final boolean offer(final E nextValue) { if (nextValue == null) { - throw new IllegalArgumentException("null elements not allowed"); + throw new NullPointerException("null elements not allowed"); } final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); final LinkedQueueNode prevProducerNode = xchgProducerNode(nextNode); diff --git a/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java b/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java index 5832f7371d..deb1ff7b68 100644 --- a/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java +++ b/src/main/java/rx/internal/util/atomic/SpscLinkedAtomicQueue.java @@ -59,7 +59,7 @@ public SpscLinkedAtomicQueue() { @Override public boolean offer(final E nextValue) { if (nextValue == null) { - throw new IllegalArgumentException("null elements not allowed"); + throw new NullPointerException("null elements not allowed"); } final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); lpProducerNode().soNext(nextNode); diff --git a/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java b/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java index 8333723036..a75c2a0028 100644 --- a/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/MpmcArrayQueue.java @@ -28,15 +28,7 @@ public MpmcArrayQueueL1Pad(int capacity) { } abstract class MpmcArrayQueueProducerField extends MpmcArrayQueueL1Pad { - private final static long P_INDEX_OFFSET; - static { - try { - P_INDEX_OFFSET = UNSAFE.objectFieldOffset(MpmcArrayQueueProducerField.class - .getDeclaredField("producerIndex")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } + private final static long P_INDEX_OFFSET = UnsafeAccess.addressOf(MpmcArrayQueueProducerField.class, "producerIndex"); private volatile long producerIndex; public MpmcArrayQueueProducerField(int capacity) { @@ -62,15 +54,7 @@ public MpmcArrayQueueL2Pad(int capacity) { } abstract class MpmcArrayQueueConsumerField extends MpmcArrayQueueL2Pad { - private final static long C_INDEX_OFFSET; - static { - try { - C_INDEX_OFFSET = UNSAFE.objectFieldOffset(MpmcArrayQueueConsumerField.class - .getDeclaredField("consumerIndex")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } + private final static long C_INDEX_OFFSET = UnsafeAccess.addressOf(MpmcArrayQueueConsumerField.class, "consumerIndex"); private volatile long consumerIndex; public MpmcArrayQueueConsumerField(int capacity) { diff --git a/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java index f9e63f1c6b..2607c3a023 100644 --- a/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java +++ b/src/main/java/rx/internal/util/unsafe/MpscLinkedQueue.java @@ -69,7 +69,7 @@ protected final LinkedQueueNode xchgProducerNode(LinkedQueueNode newVal) { @Override public final boolean offer(final E nextValue) { if (nextValue == null) { - throw new IllegalArgumentException("null elements not allowed"); + throw new NullPointerException("null elements not allowed"); } final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); final LinkedQueueNode prevProducerNode = xchgProducerNode(nextNode); diff --git a/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java index ebf79f0708..8a4251872d 100644 --- a/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpmcArrayQueue.java @@ -28,15 +28,7 @@ public SpmcArrayQueueL1Pad(int capacity) { } abstract class SpmcArrayQueueProducerField extends SpmcArrayQueueL1Pad { - protected final static long P_INDEX_OFFSET; - static { - try { - P_INDEX_OFFSET = - UNSAFE.objectFieldOffset(SpmcArrayQueueProducerField.class.getDeclaredField("producerIndex")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } + protected final static long P_INDEX_OFFSET = UnsafeAccess.addressOf(SpmcArrayQueueProducerField.class, "producerIndex"); private volatile long producerIndex; protected final long lvProducerIndex() { @@ -62,15 +54,7 @@ public SpmcArrayQueueL2Pad(int capacity) { } abstract class SpmcArrayQueueConsumerField extends SpmcArrayQueueL2Pad { - protected final static long C_INDEX_OFFSET; - static { - try { - C_INDEX_OFFSET = - UNSAFE.objectFieldOffset(SpmcArrayQueueConsumerField.class.getDeclaredField("consumerIndex")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } + protected final static long C_INDEX_OFFSET = UnsafeAccess.addressOf(SpmcArrayQueueConsumerField.class, "consumerIndex"); private volatile long consumerIndex; public SpmcArrayQueueConsumerField(int capacity) { diff --git a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java index 16d40b6951..88c6d491c6 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java @@ -36,15 +36,7 @@ public SpscArrayQueueL1Pad(int capacity) { } abstract class SpscArrayQueueProducerFields extends SpscArrayQueueL1Pad { - protected final static long P_INDEX_OFFSET; - static { - try { - P_INDEX_OFFSET = - UNSAFE.objectFieldOffset(SpscArrayQueueProducerFields.class.getDeclaredField("producerIndex")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } + protected final static long P_INDEX_OFFSET = UnsafeAccess.addressOf(SpscArrayQueueProducerFields.class, "producerIndex"); protected long producerIndex; protected long producerLookAhead; @@ -64,15 +56,7 @@ public SpscArrayQueueL2Pad(int capacity) { abstract class SpscArrayQueueConsumerField extends SpscArrayQueueL2Pad { protected long consumerIndex; - protected final static long C_INDEX_OFFSET; - static { - try { - C_INDEX_OFFSET = - UNSAFE.objectFieldOffset(SpscArrayQueueConsumerField.class.getDeclaredField("consumerIndex")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } + protected final static long C_INDEX_OFFSET = UnsafeAccess.addressOf(SpscArrayQueueConsumerField.class, "consumerIndex"); public SpscArrayQueueConsumerField(int capacity) { super(capacity); } @@ -116,6 +100,9 @@ public SpscArrayQueue(final int capacity) { */ @Override public boolean offer(final E e) { + if (e == null) { + throw new NullPointerException("null elements not allowed"); + } // local load of field to avoid repeated loads after volatile reads final E[] lElementBuffer = buffer; final long index = producerIndex; diff --git a/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java b/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java index 7c3c675b48..b9a037a986 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscLinkedQueue.java @@ -62,7 +62,7 @@ public SpscLinkedQueue() { @Override public boolean offer(final E nextValue) { if (nextValue == null) { - throw new IllegalArgumentException("null elements not allowed"); + throw new NullPointerException("null elements not allowed"); } final LinkedQueueNode nextNode = new LinkedQueueNode(nextValue); producerNode.soNext(nextNode); diff --git a/src/test/java/rx/internal/util/JCToolsQueueTests.java b/src/test/java/rx/internal/util/JCToolsQueueTests.java index 2645dcd1c1..fea60217eb 100644 --- a/src/test/java/rx/internal/util/JCToolsQueueTests.java +++ b/src/test/java/rx/internal/util/JCToolsQueueTests.java @@ -17,13 +17,94 @@ import static org.junit.Assert.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + import org.junit.Test; +import rx.internal.util.atomic.*; import rx.internal.util.unsafe.*; public class JCToolsQueueTests { + static final class IntField { + int value; + } + static void await(CyclicBarrier cb) { + try { + cb.await(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } catch (BrokenBarrierException ex) { + throw new RuntimeException(ex); + } + } + @Test + public void casBasedUnsafe() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + long offset = UnsafeAccess.addressOf(IntField.class, "value"); + IntField f = new IntField(); + + assertTrue(UnsafeAccess.compareAndSwapInt(f, offset, 0, 1)); + assertFalse(UnsafeAccess.compareAndSwapInt(f, offset, 0, 2)); + + assertEquals(1, UnsafeAccess.getAndAddInt(f, offset, 2)); + + assertEquals(3, UnsafeAccess.getAndIncrementInt(f, offset)); + + assertEquals(4, UnsafeAccess.getAndSetInt(f, offset, 0)); + } + + @Test + public void powerOfTwo() { + assertTrue(Pow2.isPowerOfTwo(1)); + assertTrue(Pow2.isPowerOfTwo(2)); + assertFalse(Pow2.isPowerOfTwo(3)); + assertTrue(Pow2.isPowerOfTwo(4)); + assertFalse(Pow2.isPowerOfTwo(5)); + assertTrue(Pow2.isPowerOfTwo(8)); + assertFalse(Pow2.isPowerOfTwo(13)); + assertTrue(Pow2.isPowerOfTwo(16)); + assertFalse(Pow2.isPowerOfTwo(25)); + assertFalse(Pow2.isPowerOfTwo(31)); + assertTrue(Pow2.isPowerOfTwo(32)); + } + + @Test(expected = NullPointerException.class) + public void testMpmcArrayQueueNull() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + MpmcArrayQueue q = new MpmcArrayQueue(16); + q.offer(null); + } + + @Test(expected = UnsupportedOperationException.class) + public void testMpmcArrayQueueIterator() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + MpmcArrayQueue q = new MpmcArrayQueue(16); + q.iterator(); + } + + @Test + public void testMpmcArrayQueueOfferPoll() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + Queue q = new MpmcArrayQueue(128); + + testOfferPoll(q); + } + @Test public void testMpmcOfferUpToCapacity() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } int n = 128; MpmcArrayQueue queue = new MpmcArrayQueue(n); for (int i = 0; i < n; i++) { @@ -31,22 +112,352 @@ public void testMpmcOfferUpToCapacity() { } assertFalse(queue.offer(n)); } + @Test(expected = UnsupportedOperationException.class) + public void testMpscLinkedAtomicQueueIterator() { + MpscLinkedAtomicQueue q = new MpscLinkedAtomicQueue(); + q.iterator(); + } + + @Test(expected = NullPointerException.class) + public void testMpscLinkedAtomicQueueNull() { + MpscLinkedAtomicQueue q = new MpscLinkedAtomicQueue(); + q.offer(null); + } + @Test - public void testSpscOfferUpToCapacity() { + public void testMpscLinkedAtomicQueueOfferPoll() { + MpscLinkedAtomicQueue q = new MpscLinkedAtomicQueue(); + + testOfferPoll(q); + } + + @Test(timeout = 2000) + public void testMpscLinkedAtomicQueuePipelined() throws InterruptedException { + final MpscLinkedAtomicQueue q = new MpscLinkedAtomicQueue(); + + Set set = new HashSet(); + for (int i = 0; i < 1000 * 1000; i++) { + set.add(i); + } + + final CyclicBarrier cb = new CyclicBarrier(3); + + Thread t1 = new Thread(new Runnable() { + @Override + public void run() { + await(cb); + for (int i = 0; i < 500 * 1000; i++) { + q.offer(i); + } + } + }); + Thread t2 = new Thread(new Runnable() { + @Override + public void run() { + await(cb); + for (int i = 500 * 1000; i < 1000 * 1000; i++) { + q.offer(i); + } + } + }); + + t1.start(); + t2.start(); + + await(cb); + + Integer j; + for (int i = 0; i < 1000 * 1000; i++) { + while ((j = q.poll()) == null); + assertTrue("Value " + j + " already removed", set.remove(j)); + } + assertTrue("Set is not empty", set.isEmpty()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testMpscLinkedQueueIterator() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + MpscLinkedQueue q = new MpscLinkedQueue(); + q.iterator(); + } + + @Test(expected = NullPointerException.class) + public void testMpscLinkedQueueNull() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + MpscLinkedQueue q = new MpscLinkedQueue(); + q.offer(null); + } + + @Test + public void testMpscLinkedQueueOfferPoll() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + MpscLinkedQueue q = new MpscLinkedQueue(); + + testOfferPoll(q); + } + @Test(timeout = 2000) + public void testMpscLinkedQueuePipelined() throws InterruptedException { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + final MpscLinkedQueue q = new MpscLinkedQueue(); + + Set set = new HashSet(); + for (int i = 0; i < 1000 * 1000; i++) { + set.add(i); + } + + final CyclicBarrier cb = new CyclicBarrier(3); + + Thread t1 = new Thread(new Runnable() { + @Override + public void run() { + await(cb); + for (int i = 0; i < 500 * 1000; i++) { + q.offer(i); + } + } + }); + Thread t2 = new Thread(new Runnable() { + @Override + public void run() { + await(cb); + for (int i = 500 * 1000; i < 1000 * 1000; i++) { + q.offer(i); + } + } + }); + + t1.start(); + t2.start(); + + await(cb); + + Integer j; + for (int i = 0; i < 1000 * 1000; i++) { + while ((j = q.poll()) == null); + assertTrue("Value " + j + " already removed", set.remove(j)); + } + assertTrue("Set is not empty", set.isEmpty()); + } + + protected void testOfferPoll(Queue q) { + for (int i = 0; i < 64; i++) { + assertTrue(q.offer(i)); + } + assertFalse(q.isEmpty()); + for (int i = 0; i < 64; i++) { + assertEquals((Integer)i, q.peek()); + + assertEquals(64 - i, q.size()); + + assertEquals((Integer)i, q.poll()); + } + assertTrue(q.isEmpty()); + + for (int i = 0; i < 64; i++) { + assertTrue(q.offer(i)); + assertEquals((Integer)i, q.poll()); + } + + assertTrue(q.isEmpty()); + assertNull(q.peek()); + assertNull(q.poll()); + } + + @Test(expected = NullPointerException.class) + public void testSpmcArrayQueueNull() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + SpmcArrayQueue q = new SpmcArrayQueue(16); + q.offer(null); + } + + @Test + public void testSpmcArrayQueueOfferPoll() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + Queue q = new SpmcArrayQueue(128); + + testOfferPoll(q); + } + @Test(expected = UnsupportedOperationException.class) + public void testSpmcArrayQueueIterator() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + SpmcArrayQueue q = new SpmcArrayQueue(16); + q.iterator(); + } + + @Test + public void testSpmcOfferUpToCapacity() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } int n = 128; - SpscArrayQueue queue = new SpscArrayQueue(n); + SpmcArrayQueue queue = new SpmcArrayQueue(n); for (int i = 0; i < n; i++) { assertTrue(queue.offer(i)); } assertFalse(queue.offer(n)); } + + @Test(expected = NullPointerException.class) + public void testSpscArrayQueueNull() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + SpscArrayQueue q = new SpscArrayQueue(16); + q.offer(null); + } + @Test - public void testSpmcOfferUpToCapacity() { + public void testSpscArrayQueueOfferPoll() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + Queue q = new SpscArrayQueue(128); + + testOfferPoll(q); + } + @Test(expected = UnsupportedOperationException.class) + public void testSpscArrayQueueIterator() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + SpscArrayQueue q = new SpscArrayQueue(16); + q.iterator(); + } + @Test(expected = UnsupportedOperationException.class) + public void testSpscLinkedAtomicQueueIterator() { + SpscLinkedAtomicQueue q = new SpscLinkedAtomicQueue(); + q.iterator(); + } + @Test(expected = NullPointerException.class) + public void testSpscLinkedAtomicQueueNull() { + SpscLinkedAtomicQueue q = new SpscLinkedAtomicQueue(); + q.offer(null); + } + + @Test + public void testSpscLinkedAtomicQueueOfferPoll() { + SpscLinkedAtomicQueue q = new SpscLinkedAtomicQueue(); + + testOfferPoll(q); + } + + @Test(timeout = 2000) + public void testSpscLinkedAtomicQueuePipelined() throws InterruptedException { + final SpscLinkedAtomicQueue q = new SpscLinkedAtomicQueue(); + final AtomicInteger count = new AtomicInteger(); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + Integer j; + for (int i = 0; i < 1000 * 1000; i++) { + while ((j = q.poll()) == null); + if (j == i) { + count.getAndIncrement(); + } + } + } + }); + t.start(); + + for (int i = 0; i < 1000 * 1000; i++) { + assertTrue(q.offer(i)); + } + t.join(); + + assertEquals(1000 * 1000, count.get()); + } + + @Test(expected = UnsupportedOperationException.class) + public void testSpscLinkedQueueIterator() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + SpscLinkedQueue q = new SpscLinkedQueue(); + q.iterator(); + } + + @Test(expected = NullPointerException.class) + public void testSpscLinkedQueueNull() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + SpscLinkedQueue q = new SpscLinkedQueue(); + q.offer(null); + } + + @Test + public void testSpscLinkedQueueOfferPoll() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + SpscLinkedQueue q = new SpscLinkedQueue(); + + testOfferPoll(q); + } + + @Test(timeout = 2000) + public void testSpscLinkedQueuePipelined() throws InterruptedException { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + final SpscLinkedQueue q = new SpscLinkedQueue(); + final AtomicInteger count = new AtomicInteger(); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + Integer j; + for (int i = 0; i < 1000 * 1000; i++) { + while ((j = q.poll()) == null); + if (j == i) { + count.getAndIncrement(); + } + } + } + }); + t.start(); + + for (int i = 0; i < 1000 * 1000; i++) { + assertTrue(q.offer(i)); + } + t.join(); + + assertEquals(1000 * 1000, count.get()); + } + + @Test + public void testSpscOfferUpToCapacity() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } int n = 128; - SpmcArrayQueue queue = new SpmcArrayQueue(n); + SpscArrayQueue queue = new SpscArrayQueue(n); for (int i = 0; i < n; i++) { assertTrue(queue.offer(i)); } assertFalse(queue.offer(n)); } + + @Test(expected = InternalError.class) + public void testUnsafeAccessAddressOf() { + if (!UnsafeAccess.isUnsafeAvailable()) { + return; + } + UnsafeAccess.addressOf(Object.class, "field"); + } } From cc977395ee2a698b3e5c50fe2c722e2a5efffe1d Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 23 Jul 2015 16:21:41 +1000 Subject: [PATCH 346/857] add backpressure to OperatorMaterialize --- .../operators/OperatorMaterialize.java | 133 +++++++++++++++--- .../operators/OperatorMaterializeTest.java | 114 ++++++++++++++- 2 files changed, 227 insertions(+), 20 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMaterialize.java b/src/main/java/rx/internal/operators/OperatorMaterialize.java index bd5771747c..e074cd5816 100644 --- a/src/main/java/rx/internal/operators/OperatorMaterialize.java +++ b/src/main/java/rx/internal/operators/OperatorMaterialize.java @@ -15,8 +15,11 @@ */ package rx.internal.operators; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + import rx.Notification; import rx.Observable.Operator; +import rx.Producer; import rx.Subscriber; import rx.plugins.RxJavaPlugins; @@ -29,41 +32,137 @@ * See here for the Microsoft Rx equivalent. */ public final class OperatorMaterialize implements Operator, T> { + /** Lazy initialization via inner-class holder. */ private static final class Holder { /** A singleton instance. */ static final OperatorMaterialize INSTANCE = new OperatorMaterialize(); } + /** * @return a singleton instance of this stateless operator. */ @SuppressWarnings("unchecked") public static OperatorMaterialize instance() { - return (OperatorMaterialize)Holder.INSTANCE; + return (OperatorMaterialize) Holder.INSTANCE; } - private OperatorMaterialize() { } + + private OperatorMaterialize() { + } + @Override public Subscriber call(final Subscriber> child) { - return new Subscriber(child) { - + final ParentSubscriber parent = new ParentSubscriber(child); + child.add(parent); + child.setProducer(new Producer() { @Override - public void onCompleted() { - child.onNext(Notification. createOnCompleted()); - child.onCompleted(); + public void request(long n) { + if (n > 0) { + parent.requestMore(n); + } } + }); + return parent; + } - @Override - public void onError(Throwable e) { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); - child.onNext(Notification. createOnError(e)); - child.onCompleted(); - } + private static class ParentSubscriber extends Subscriber { - @Override - public void onNext(T t) { - child.onNext(Notification. createOnNext(t)); + private final Subscriber> child; + + private volatile Notification terminalNotification; + + // guarded by this + private boolean busy = false; + // guarded by this + private boolean missed = false; + + private volatile long requested; + @SuppressWarnings("rawtypes") + private static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater + .newUpdater(ParentSubscriber.class, "requested"); + + ParentSubscriber(Subscriber> child) { + this.child = child; + } + + @Override + public void onStart() { + request(0); + } + + void requestMore(long n) { + BackpressureUtils.getAndAddRequest(REQUESTED, this, n); + request(n); + drain(); + } + + @Override + public void onCompleted() { + terminalNotification = Notification.createOnCompleted(); + drain(); + } + + @Override + public void onError(Throwable e) { + terminalNotification = Notification.createOnError(e); + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + drain(); + } + + @Override + public void onNext(T t) { + child.onNext(Notification.createOnNext(t)); + decrementRequested(); + } + + private void decrementRequested() { + // atomically decrement requested + while (true) { + long r = requested; + if (r == Long.MAX_VALUE) { + // don't decrement if unlimited requested + return; + } else if (REQUESTED.compareAndSet(this, r, r - 1)) { + return; + } } + } - }; + private void drain() { + synchronized (this) { + if (busy) { + // set flag to force extra loop if drain loop running + missed = true; + return; + } + } + // drain loop + while (!child.isUnsubscribed()) { + Notification tn; + tn = terminalNotification; + if (tn != null) { + if (requested > 0) { + // allow tn to be GC'd after the onNext call + terminalNotification = null; + // emit the terminal notification + child.onNext(tn); + if (!child.isUnsubscribed()) { + child.onCompleted(); + } + // note that we leave busy=true here + // which will prevent further drains + return; + } + } + // continue looping if drain() was called while in + // this loop + synchronized (this) { + if (!missed) { + busy = false; + return; + } + } + } + } } } diff --git a/src/test/java/rx/internal/operators/OperatorMaterializeTest.java b/src/test/java/rx/internal/operators/OperatorMaterializeTest.java index a900da61d6..511a79ed54 100644 --- a/src/test/java/rx/internal/operators/OperatorMaterializeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMaterializeTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.Arrays; import java.util.List; import java.util.Vector; import java.util.concurrent.ExecutionException; @@ -28,13 +29,18 @@ import rx.Notification; import rx.Observable; import rx.Subscriber; +import rx.functions.Action1; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; public class OperatorMaterializeTest { @Test public void testMaterialize1() { - // null will cause onError to be triggered before "three" can be returned - final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", null, "three"); + // null will cause onError to be triggered before "three" can be + // returned + final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", null, + "three"); TestObserver Observer = new TestObserver(); Observable> m = Observable.create(o1).materialize(); @@ -53,7 +59,8 @@ public void testMaterialize1() { assertTrue(Observer.notifications.get(0).isOnNext()); assertEquals("two", Observer.notifications.get(1).getValue()); assertTrue(Observer.notifications.get(1).isOnNext()); - assertEquals(NullPointerException.class, Observer.notifications.get(2).getThrowable().getClass()); + assertEquals(NullPointerException.class, Observer.notifications.get(2).getThrowable() + .getClass()); assertTrue(Observer.notifications.get(2).isOnError()); } @@ -93,6 +100,107 @@ public void testMultipleSubscribes() throws InterruptedException, ExecutionExcep assertEquals(3, m.toList().toBlocking().toFuture().get().size()); } + @Test + public void testBackpressureOnEmptyStream() { + TestSubscriber> ts = TestSubscriber.create(0); + Observable. empty().materialize().subscribe(ts); + ts.assertNoValues(); + ts.requestMore(1); + ts.assertValueCount(1); + assertTrue(ts.getOnNextEvents().get(0).isOnCompleted()); + ts.assertCompleted(); + } + + @Test + public void testBackpressureNoError() { + TestSubscriber> ts = TestSubscriber.create(0); + Observable.just(1, 2, 3).materialize().subscribe(ts); + ts.assertNoValues(); + ts.requestMore(1); + ts.assertValueCount(1); + ts.requestMore(2); + ts.assertValueCount(3); + ts.requestMore(1); + ts.assertValueCount(4); + ts.assertCompleted(); + } + + @Test + public void testBackpressureNoErrorAsync() throws InterruptedException { + TestSubscriber> ts = TestSubscriber.create(0); + Observable.just(1, 2, 3) + .materialize() + .subscribeOn(Schedulers.computation()) + .subscribe(ts); + Thread.sleep(100); + ts.assertNoValues(); + ts.requestMore(1); + Thread.sleep(100); + ts.assertValueCount(1); + ts.requestMore(2); + Thread.sleep(100); + ts.assertValueCount(3); + ts.requestMore(1); + Thread.sleep(100); + ts.assertValueCount(4); + ts.assertCompleted(); + } + + @Test + public void testBackpressureWithError() { + TestSubscriber> ts = TestSubscriber.create(0); + Observable. error(new IllegalArgumentException()).materialize().subscribe(ts); + ts.assertNoValues(); + ts.requestMore(1); + ts.assertValueCount(1); + ts.assertCompleted(); + } + + @Test + public void testBackpressureWithEmissionThenError() { + TestSubscriber> ts = TestSubscriber.create(0); + IllegalArgumentException ex = new IllegalArgumentException(); + Observable.from(Arrays.asList(1)).concatWith(Observable. error(ex)).materialize() + .subscribe(ts); + ts.assertNoValues(); + ts.requestMore(1); + ts.assertValueCount(1); + assertTrue(ts.getOnNextEvents().get(0).hasValue()); + ts.requestMore(1); + ts.assertValueCount(2); + assertTrue(ts.getOnNextEvents().get(1).isOnError()); + assertTrue(ex == ts.getOnNextEvents().get(1).getThrowable()); + ts.assertCompleted(); + } + + @Test + public void testWithCompletionCausingError() { + TestSubscriber> ts = TestSubscriber.create(); + final RuntimeException ex = new RuntimeException("boo"); + Observable.empty().materialize().doOnNext(new Action1() { + @Override + public void call(Object t) { + throw ex; + } + }).subscribe(ts); + ts.assertError(ex); + ts.assertNoValues(); + ts.assertTerminalEvent(); + } + + @Test + public void testUnsubscribeJustBeforeCompletionNotificationShouldPreventThatNotificationArriving() { + TestSubscriber> ts = TestSubscriber.create(0); + IllegalArgumentException ex = new IllegalArgumentException(); + Observable.empty().materialize() + .subscribe(ts); + ts.assertNoValues(); + ts.unsubscribe(); + ts.requestMore(1); + ts.assertNoValues(); + ts.assertUnsubscribed(); + } + private static class TestObserver extends Subscriber> { boolean onCompleted = false; From 6aa442a0fcb31a16458f3fd8802e110a20898ec8 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 24 Jul 2015 12:32:29 +0200 Subject: [PATCH 347/857] Test coverage of rx.functions utility methods. --- src/test/java/rx/functions/ActionsTest.java | 290 ++++++++++++++++++ src/test/java/rx/functions/FunctionsTest.java | 268 ++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 src/test/java/rx/functions/ActionsTest.java create mode 100644 src/test/java/rx/functions/FunctionsTest.java diff --git a/src/test/java/rx/functions/ActionsTest.java b/src/test/java/rx/functions/ActionsTest.java new file mode 100644 index 0000000000..8ffecb97ca --- /dev/null +++ b/src/test/java/rx/functions/ActionsTest.java @@ -0,0 +1,290 @@ +/** + * 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.functions; + +import static org.junit.Assert.*; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; + +public class ActionsTest { + + @Test + public void testEmptyArities() { + Action0 a0 = Actions.empty(); + a0.call(); + + Action1 a1 = Actions.empty(); + a1.call(1); + + Action2 a2 = Actions.empty(); + a2.call(1, 2); + + Action3 a3 = Actions.empty(); + a3.call(1, 2, 3); + + Action4 a4 = Actions.empty(); + a4.call(1, 2, 3, 4); + + Action5 a5 = Actions.empty(); + a5.call(1, 2, 3, 4, 5); + + Action6 a6 = Actions.empty(); + a6.call(1, 2, 3, 4, 5, 6); + + Action7 a7 = Actions.empty(); + a7.call(1, 2, 3, 4, 5, 6, 7); + + Action8 a8 = Actions.empty(); + a8.call(1, 2, 3, 4, 5, 6, 7, 8); + + Action9 a9 = Actions.empty(); + a9.call(1, 2, 3, 4, 5, 6, 7, 8, 9); + + ActionN an0 = Actions.empty(); + an0.call(); + + ActionN an1 = Actions.empty(); + an1.call(1); + + ActionN ann = Actions.empty(); + ann.call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + ActionN annn = Actions.empty(); + annn.call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20); + } + + @Test + public void testToFunc0() { + final AtomicLong value = new AtomicLong(-1L); + final Action0 action = new Action0() { + @Override + public void call() { + value.set(0); + } + }; + + assertNull(Actions.toFunc(action).call()); + assertEquals(0, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call()); + assertEquals(0, value.get()); + } + + @Test + public void testToFunc1() { + final AtomicLong value = new AtomicLong(-1L); + final Action1 action = new Action1() { + @Override + public void call(Integer t1) { + value.set(t1); + } + }; + + assertNull(Actions.toFunc(action).call(1)); + assertEquals(1, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1)); + assertEquals(1, value.get()); + } + + @Test + public void testToFunc2() { + final AtomicLong value = new AtomicLong(-1L); + final Action2 action = new Action2() { + @Override + public void call(Integer t1, Integer t2) { + value.set(t1 | t2); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2)); + assertNull(Actions.toFunc(action).call(1, 2)); + assertEquals(3, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2)); + assertEquals(3, value.get()); + } + + @Test + public void testToFunc3() { + final AtomicLong value = new AtomicLong(-1L); + final Action3 action = new Action3() { + @Override + public void call(Integer t1, Integer t2, Integer t3) { + value.set(t1 | t2 | t3); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2, 4)); + assertEquals(7, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2, 4)); + assertEquals(7, value.get()); + } + + @Test + public void testToFunc4() { + final AtomicLong value = new AtomicLong(-1L); + final Action4 action = new Action4() { + @Override + public void call(Integer t1, Integer t2, Integer t3, Integer t4) { + value.set(t1 | t2 | t3 | t4); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2, 4, 8)); + assertEquals(15, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2, 4, 8)); + assertEquals(15, value.get()); + } + + @Test + public void testToFunc5() { + final AtomicLong value = new AtomicLong(-1L); + final Action5 action = + new Action5() { + @Override + public void call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5) { + value.set(t1 | t2 | t3 | t4 | t5); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2, 4, 8, 16)); + assertEquals(31, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2, 4, 8, 16)); + assertEquals(31, value.get()); + } + + @Test + public void testToFunc6() { + final AtomicLong value = new AtomicLong(-1L); + final Action6 action = + new Action6() { + @Override + public void call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6) { + value.set(t1 | t2 | t3 | t4 | t5 | t6); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2, 4, 8, 16, 32)); + assertEquals(63, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2, 4, 8, 16, 32)); + assertEquals(63, value.get()); + } + + @Test + public void testToFunc7() { + final AtomicLong value = new AtomicLong(-1L); + final Action7 action = + new Action7() { + @Override + public void call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7) { + value.set(t1 | t2 | t3 | t4 | t5 | t6 | t7); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2, 4, 8, 16, 32, 64)); + assertEquals(127, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2, 4, 8, 16, 32, 64)); + assertEquals(127, value.get()); + } + @Test + public void testToFunc8() { + final AtomicLong value = new AtomicLong(-1L); + final Action8 action = + new Action8() { + @Override + public void call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8) { + value.set(t1 | t2 | t3 | t4 | t5 | t6 | t7 | t8); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2, 4, 8, 16, 32, 64, 128)); + assertEquals(255, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2, 4, 8, 16, 32, 64, 128)); + assertEquals(255, value.get()); + } + @Test + public void testToFunc9() { + final AtomicLong value = new AtomicLong(-1L); + final Action9 action = + new Action9() { + @Override + public void call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8, Integer t9) { + value.set(t1 | t2 | t3 | t4 | t5 | t6 | t7 | t8 | t9); + } + }; + + assertNull(Actions.toFunc(action).call(1, 2, 4, 8, 16, 32, 64, 128, 256)); + assertEquals(511, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(1, 2, 4, 8, 16, 32, 64, 128, 256)); + assertEquals(511, value.get()); + } + + @Test + public void testToFuncN() { + for (int i = 0; i < 100; i++) { + final AtomicLong value = new AtomicLong(-1L); + final ActionN action = new ActionN() { + @Override + public void call(Object... args) { + int sum = 0; + for (Object o : args) { + sum += (Integer)o; + } + value.set(sum); + } + }; + Object[] arr = new Object[i]; + Arrays.fill(arr, 1); + + assertNull(Actions.toFunc(action).call(arr)); + assertEquals(i, value.get()); + value.set(-1L); + assertEquals((Integer)0, Actions.toFunc(action, 0).call(arr)); + assertEquals(i, value.get()); + } + } + + @Test + public void testNotInstantiable() { + try { + Constructor c = Actions.class.getDeclaredConstructor(); + c.setAccessible(true); + Object instance = c.newInstance(); + fail("Could instantiate Actions! " + instance); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + } catch (InvocationTargetException ex) { + ex.printStackTrace(); + } catch (InstantiationException ex) { + ex.printStackTrace(); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + } +} diff --git a/src/test/java/rx/functions/FunctionsTest.java b/src/test/java/rx/functions/FunctionsTest.java new file mode 100644 index 0000000000..1d2cc95374 --- /dev/null +++ b/src/test/java/rx/functions/FunctionsTest.java @@ -0,0 +1,268 @@ +/** + * 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.functions; + +import static org.junit.Assert.*; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; + +public class FunctionsTest { + @Test + public void testNotInstantiable() { + try { + Constructor c = Functions.class.getDeclaredConstructor(); + c.setAccessible(true); + Object instance = c.newInstance(); + fail("Could instantiate Actions! " + instance); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + } catch (InvocationTargetException ex) { + ex.printStackTrace(); + } catch (InstantiationException ex) { + ex.printStackTrace(); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + } + + @Test(expected = RuntimeException.class) + public void testFromFunc0() { + Func0 func = new Func0() { + @Override + public Integer call() { + return 0; + } + }; + + Object[] params = new Object[0]; + assertEquals((Integer)0, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc1() { + Func1 func = new Func1() { + @Override + public Integer call(Integer t1) { + return t1; + } + }; + + Object[] params = new Object[] { 1 }; + assertEquals((Integer)1, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc2() { + Func2 func = new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + Object[] params = new Object[] { 1, 2 }; + assertEquals((Integer)3, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc3() { + Func3 func = new Func3() { + @Override + public Integer call(Integer t1, Integer t2, Integer t3) { + return t1 | t2 | t3; + } + }; + + Object[] params = new Object[] { 1, 2, 4 }; + assertEquals((Integer)7, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc4() { + Func4 func = + new Func4() { + @Override + public Integer call(Integer t1, Integer t2, Integer t3, Integer t4) { + return t1 | t2 | t3 | t4; + } + }; + + Object[] params = new Object[] { 1, 2, 4, 8 }; + assertEquals((Integer)15, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc5() { + Func5 func = + new Func5() { + @Override + public Integer call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5) { + return t1 | t2 | t3 | t4 | t5; + } + }; + + Object[] params = new Object[] { 1, 2, 4, 8, 16 }; + assertEquals((Integer)31, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc6() { + Func6 func = + new Func6() { + @Override + public Integer call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6) { + return t1 | t2 | t3 | t4 | t5 | t6; + } + }; + + Object[] params = new Object[] { 1, 2, 4, 8, 16, 32 }; + assertEquals((Integer)63, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc7() { + Func7 func = + new Func7() { + @Override + public Integer call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7) { + return t1 | t2 | t3 | t4 | t5 | t6 | t7; + } + }; + + Object[] params = new Object[] { 1, 2, 4, 8, 16, 32, 64 }; + assertEquals((Integer)127, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc8() { + Func8 func = + new Func8() { + @Override + public Integer call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8) { + return t1 | t2 | t3 | t4 | t5 | t6 | t7 | t8; + } + }; + + Object[] params = new Object[] { 1, 2, 4, 8, 16, 32, 64, 128 }; + assertEquals((Integer)255, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromFunc9() { + Func9 func = + new Func9() { + @Override + public Integer call(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8, Integer t9) { + return t1 | t2 | t3 | t4 | t5 | t6 | t7 | t8 | t9; + } + }; + + Object[] params = new Object[] { 1, 2, 4, 8, 16, 32, 64, 128, 256 }; + assertEquals((Integer)511, Functions.fromFunc(func).call(params)); + + Functions.fromFunc(func).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromAction0() { + final AtomicLong value = new AtomicLong(); + Action0 action = new Action0() { + @Override + public void call() { + value.set(0); + } + }; + + Object[] params = new Object[] { }; + Functions.fromAction(action).call(params); + assertEquals(0, value.get()); + + Functions.fromAction(action).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromAction1() { + final AtomicLong value = new AtomicLong(); + Action1 action = new Action1() { + @Override + public void call(Integer t1) { + value.set(t1); + } + }; + + Object[] params = new Object[] { 1 }; + Functions.fromAction(action).call(params); + assertEquals(1, value.get()); + + Functions.fromAction(action).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromAction2() { + final AtomicLong value = new AtomicLong(); + Action2 action = new Action2() { + @Override + public void call(Integer t1, Integer t2) { + value.set(t1 | t2); + } + }; + + Object[] params = new Object[] { 1, 2 }; + Functions.fromAction(action).call(params); + assertEquals(3, value.get()); + + Functions.fromAction(action).call(Arrays.copyOf(params, params.length + 1)); + } + + @Test(expected = RuntimeException.class) + public void testFromAction3() { + final AtomicLong value = new AtomicLong(); + Action3 action = new Action3() { + @Override + public void call(Integer t1, Integer t2, Integer t3) { + value.set(t1 | t2 | t3); + } + }; + + Object[] params = new Object[] { 1, 2, 4 }; + Functions.fromAction(action).call(params); + assertEquals(7, value.get()); + + Functions.fromAction(action).call(Arrays.copyOf(params, params.length + 1)); + } +} From b21c237253cb585305c02fffb12b60e69caee16a Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 27 Jul 2015 08:23:56 +0200 Subject: [PATCH 348/857] cache() now supports backpressure (again) --- src/main/java/rx/Observable.java | 4 +- .../internal/operators/CachedObservable.java | 462 ++++++++++++++++++ .../internal/operators/OnSubscribeCache.java | 76 --- .../rx/internal/util/LinkedArrayList.java | 136 ++++++ .../rx/internal/util/LinkedArrayListTest.java | 37 ++ .../operators/CachedObservableTest.java | 293 +++++++++++ .../operators/OnSubscribeCacheTest.java | 164 ------- 7 files changed, 930 insertions(+), 242 deletions(-) create mode 100644 src/main/java/rx/internal/operators/CachedObservable.java delete mode 100644 src/main/java/rx/internal/operators/OnSubscribeCache.java create mode 100644 src/main/java/rx/internal/util/LinkedArrayList.java create mode 100644 src/main/java/rx/internal/util/LinkedArrayListTest.java create mode 100644 src/test/java/rx/internal/operators/CachedObservableTest.java delete mode 100644 src/test/java/rx/internal/operators/OnSubscribeCacheTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 9fd476eab7..a6aabc082e 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3599,7 +3599,7 @@ public final Observable> buffer(Observable boundary, int initialC * @see ReactiveX operators documentation: Replay */ public final Observable cache() { - return create(new OnSubscribeCache(this)); + return CachedObservable.from(this); } /** @@ -3634,7 +3634,7 @@ public final Observable cache() { * @see ReactiveX operators documentation: Replay */ public final Observable cache(int capacityHint) { - return create(new OnSubscribeCache(this, capacityHint)); + return CachedObservable.from(this, capacityHint); } /** diff --git a/src/main/java/rx/internal/operators/CachedObservable.java b/src/main/java/rx/internal/operators/CachedObservable.java new file mode 100644 index 0000000000..0231c3590f --- /dev/null +++ b/src/main/java/rx/internal/operators/CachedObservable.java @@ -0,0 +1,462 @@ +/** + * 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.atomic.*; + +import rx.*; +import rx.exceptions.*; +import rx.internal.util.LinkedArrayList; +import rx.subscriptions.SerialSubscription; + +/** + * An observable which auto-connects to another observable, caches the elements + * from that observable but allows terminating the connection and completing the cache. + * + * @param the source element type + */ +public final class CachedObservable extends Observable { + /** The cache and replay state. */ + private CacheState state; + + /** + * Creates a cached Observable with a default capacity hint of 16. + * @param source the source Observable to cache + * @return the CachedObservable instance + */ + public static CachedObservable from(Observable source) { + return from(source, 16); + } + + /** + * Creates a cached Observable with the given capacity hint. + * @param source the source Observable to cache + * @param capacityHint the hint for the internal buffer size + * @return the CachedObservable instance + */ + public static CachedObservable from(Observable source, int capacityHint) { + if (capacityHint < 1) { + throw new IllegalArgumentException("capacityHint > 0 required"); + } + CacheState state = new CacheState(source, capacityHint); + CachedSubscribe onSubscribe = new CachedSubscribe(state); + return new CachedObservable(onSubscribe, state); + } + + /** + * Private constructor because state needs to be shared between the Observable body and + * the onSubscribe function. + * @param onSubscribe + * @param state + */ + private CachedObservable(OnSubscribe onSubscribe, CacheState state) { + super(onSubscribe); + this.state = state; + } + + /** + * Check if this cached observable is connected to its source. + * @return true if already connected + */ + /* public */boolean isConnected() { + return state.isConnected; + } + + /** + * Returns true if there are observers subscribed to this observable. + * @return + */ + /* public */ boolean hasObservers() { + return state.producers.length != 0; + } + + /** + * Returns the number of events currently cached. + * @return + */ + /* public */ int cachedEventCount() { + return state.size(); + } + + /** + * Contains the active child producers and the values to replay. + * + * @param + */ + static final class CacheState extends LinkedArrayList implements Observer { + /** The source observable to connect to. */ + final Observable source; + /** Holds onto the subscriber connected to source. */ + final SerialSubscription connection; + /** Guarded by connection (not this). */ + volatile ReplayProducer[] producers; + /** The default empty array of producers. */ + static final ReplayProducer[] EMPTY = new ReplayProducer[0]; + + final NotificationLite nl; + + /** Set to true after connection. */ + volatile boolean isConnected; + /** + * Indicates that the source has completed emitting values or the + * Observable was forcefully terminated. + */ + boolean sourceDone; + + public CacheState(Observable source, int capacityHint) { + super(capacityHint); + this.source = source; + this.producers = EMPTY; + this.nl = NotificationLite.instance(); + this.connection = new SerialSubscription(); + } + /** + * Adds a ReplayProducer to the producers array atomically. + * @param p + */ + public void addProducer(ReplayProducer p) { + // guarding by connection to save on allocating another object + // thus there are two distinct locks guarding the value-addition and child come-and-go + synchronized (connection) { + ReplayProducer[] a = producers; + int n = a.length; + ReplayProducer[] b = new ReplayProducer[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = p; + producers = b; + } + } + /** + * Removes the ReplayProducer (if present) from the producers array atomically. + * @param p + */ + public void removeProducer(ReplayProducer p) { + synchronized (connection) { + ReplayProducer[] a = producers; + int n = a.length; + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i].equals(p)) { + j = i; + break; + } + } + if (j < 0) { + return; + } + if (n == 1) { + producers = EMPTY; + return; + } + ReplayProducer[] b = new ReplayProducer[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + producers = b; + } + } + /** + * Connects the cache to the source. + * Make sure this is called only once. + */ + public void connect() { + Subscriber subscriber = new Subscriber() { + @Override + public void onNext(T t) { + CacheState.this.onNext(t); + } + @Override + public void onError(Throwable e) { + CacheState.this.onError(e); + } + @Override + public void onCompleted() { + CacheState.this.onCompleted(); + } + }; + connection.set(subscriber); + source.unsafeSubscribe(subscriber); + isConnected = true; + } + @Override + public void onNext(T t) { + if (!sourceDone) { + Object o = nl.next(t); + add(o); + dispatch(); + } + } + @Override + public void onError(Throwable e) { + if (!sourceDone) { + sourceDone = true; + Object o = nl.error(e); + add(o); + connection.unsubscribe(); + dispatch(); + } + } + @Override + public void onCompleted() { + if (!sourceDone) { + sourceDone = true; + Object o = nl.completed(); + add(o); + connection.unsubscribe(); + dispatch(); + } + } + /** + * Signals all known children there is work to do. + */ + void dispatch() { + ReplayProducer[] a = producers; + for (ReplayProducer rp : a) { + rp.replay(); + } + } + } + + /** + * Manages the subscription of child subscribers by setting up a replay producer and + * performs auto-connection of the very first subscription. + * @param the value type emitted + */ + static final class CachedSubscribe extends AtomicBoolean implements OnSubscribe { + /** */ + private static final long serialVersionUID = -2817751667698696782L; + final CacheState state; + public CachedSubscribe(CacheState state) { + this.state = state; + } + @Override + public void call(Subscriber t) { + // we can connect first because we replay everything anyway + ReplayProducer rp = new ReplayProducer(t, state); + state.addProducer(rp); + + t.add(rp); + t.setProducer(rp); + + // we ensure a single connection here to save an instance field of AtomicBoolean in state. + if (!get() && compareAndSet(false, true)) { + state.connect(); + } + + // no need to call rp.replay() here because the very first request will trigger it anyway + } + } + + /** + * Keeps track of the current request amount and the replay position for a child Subscriber. + * + * @param + */ + static final class ReplayProducer extends AtomicLong implements Producer, Subscription { + /** */ + private static final long serialVersionUID = -2557562030197141021L; + /** The actual child subscriber. */ + final Subscriber child; + /** The cache state object. */ + final CacheState state; + + /** + * Contains the reference to the buffer segment in replay. + * Accessed after reading state.size() and when emitting == true. + */ + Object[] currentBuffer; + /** + * Contains the index into the currentBuffer where the next value is expected. + * Accessed after reading state.size() and when emitting == true. + */ + int currentIndexInBuffer; + /** + * Contains the absolute index up until the values have been replayed so far. + */ + int index; + + /** Indicates there is a replay going on; guarded by this. */ + boolean emitting; + /** Indicates there were some state changes/replay attempts; guarded by this. */ + boolean missed; + + public ReplayProducer(Subscriber child, CacheState state) { + this.child = child; + this.state = state; + } + @Override + public void request(long n) { + for (;;) { + long r = get(); + if (r < 0) { + return; + } + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + if (compareAndSet(r, u)) { + replay(); + return; + } + } + } + /** + * Updates the request count to reflect values have been produced. + * @param n + * @return + */ + public long produced(long n) { + return addAndGet(-n); + } + + @Override + public boolean isUnsubscribed() { + return get() < 0; + } + @Override + public void unsubscribe() { + long r = get(); + if (r >= 0) { + r = getAndSet(-1L); // unsubscribed state is negative + if (r >= 0) { + state.removeProducer(this); + } + } + } + + /** + * Continue replaying available values if there are requests for them. + */ + public void replay() { + // make sure there is only a single thread emitting + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + } + boolean skipFinal = false; + try { + final NotificationLite nl = state.nl; + final Subscriber child = this.child; + + for (;;) { + + long r = get(); + + if (r < 0L) { + skipFinal = true; + return; + } + + // read the size, if it is non-zero, we can safely read the head and + // read values up to the given absolute index + int s = state.size(); + if (s != 0) { + Object[] b = currentBuffer; + + // latch onto the very first buffer now that it is available. + if (b == null) { + b = state.head(); + currentBuffer = b; + } + final int n = b.length - 1; + int j = index; + int k = currentIndexInBuffer; + // eagerly emit any terminal event + if (r == 0) { + Object o = b[k]; + if (nl.isCompleted(o)) { + child.onCompleted(); + skipFinal = true; + unsubscribe(); + return; + } else + if (nl.isError(o)) { + child.onError(nl.getError(o)); + skipFinal = true; + unsubscribe(); + return; + } + } else + if (r > 0) { + int valuesProduced = 0; + + while (j < s && r > 0) { + if (child.isUnsubscribed()) { + skipFinal = true; + return; + } + if (k == n) { + b = (Object[])b[n]; + k = 0; + } + Object o = b[k]; + + try { + if (nl.accept(child, o)) { + skipFinal = true; + unsubscribe(); + return; + } + } catch (Throwable err) { + Exceptions.throwIfFatal(err); + skipFinal = true; + unsubscribe(); + if (!nl.isError(o) && !nl.isCompleted(o)) { + child.onError(OnErrorThrowable.addValueAsLastCause(err, nl.getValue(o))); + } + return; + } + + k++; + j++; + r--; + valuesProduced++; + } + + if (child.isUnsubscribed()) { + skipFinal = true; + return; + } + + index = j; + currentIndexInBuffer = k; + currentBuffer = b; + produced(valuesProduced); + } + } + + synchronized (this) { + if (!missed) { + emitting = false; + skipFinal = true; + return; + } + missed = false; + } + } + } finally { + if (!skipFinal) { + synchronized (this) { + emitting = false; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/OnSubscribeCache.java b/src/main/java/rx/internal/operators/OnSubscribeCache.java deleted file mode 100644 index a568fd0e0b..0000000000 --- a/src/main/java/rx/internal/operators/OnSubscribeCache.java +++ /dev/null @@ -1,76 +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.atomic.AtomicIntegerFieldUpdater; - -import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.subjects.ReplaySubject; -import rx.subjects.Subject; - -/** - * This method has similar behavior to {@link Observable#replay()} except that this auto-subscribes - * to the source Observable rather than returning a connectable Observable. - *

- * - *

- * This is useful with an Observable that you want to cache responses when you can't control the - * subscribe/unsubscribe behavior of all the Observers. - *

- * Note: You sacrifice the ability to unsubscribe from the origin when you use this operator, so be - * careful not to use this operator on Observables that emit infinite or very large numbers of - * items, as this will use up memory. - * - * @param - * the cached value type - */ -public final class OnSubscribeCache implements OnSubscribe { - protected final Observable source; - protected final Subject cache; - volatile int sourceSubscribed; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater SRC_SUBSCRIBED_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(OnSubscribeCache.class, "sourceSubscribed"); - - public OnSubscribeCache(Observable source) { - this(source, ReplaySubject. create()); - } - - public OnSubscribeCache(Observable source, int capacity) { - this(source, ReplaySubject. create(capacity)); - } - - /* accessible to tests */OnSubscribeCache(Observable source, Subject cache) { - this.source = source; - this.cache = cache; - } - - @Override - public void call(Subscriber s) { - if (SRC_SUBSCRIBED_UPDATER.compareAndSet(this, 0, 1)) { - source.subscribe(cache); - /* - * Note that we will never unsubscribe from 'source' unless we receive `onCompleted` or `onError`, - * as we want to receive and cache all of its values. - * - * This means this should never be used on an infinite or very large sequence, similar to toList(). - */ - } - cache.unsafeSubscribe(s); - } -} diff --git a/src/main/java/rx/internal/util/LinkedArrayList.java b/src/main/java/rx/internal/util/LinkedArrayList.java new file mode 100644 index 0000000000..57a1289640 --- /dev/null +++ b/src/main/java/rx/internal/util/LinkedArrayList.java @@ -0,0 +1,136 @@ +/** + * 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.util; + +import java.util.*; + +/** + * A list implementation which combines an ArrayList with a LinkedList to + * avoid copying values when the capacity needs to be increased. + *

+ * The class is non final to allow embedding it directly and thus saving on object allocation. + */ +public class LinkedArrayList { + /** The capacity of each array segment. */ + final int capacityHint; + /** + * Contains the head of the linked array list if not null. The + * length is always capacityHint + 1 and the last element is an Object[] pointing + * to the next element of the linked array list. + */ + Object[] head; + /** The tail array where new elements will be added. */ + Object[] tail; + /** + * The total size of the list; written after elements have been added (release) and + * and when read, the value indicates how many elements can be safely read (acquire). + */ + volatile int size; + /** The next available slot in the current tail. */ + int indexInTail; + /** + * Constructor with the capacity hint of each array segment. + * @param capacityHint + */ + public LinkedArrayList(int capacityHint) { + this.capacityHint = capacityHint; + } + /** + * Adds a new element to this list. + * @param o the object to add, nulls are accepted + */ + public void add(Object o) { + // if no value yet, create the first array + if (size == 0) { + head = new Object[capacityHint + 1]; + tail = head; + head[0] = o; + indexInTail = 1; + size = 1; + } else + // if the tail is full, create a new tail and link + if (indexInTail == capacityHint) { + Object[] t = new Object[capacityHint + 1]; + t[0] = o; + tail[capacityHint] = t; + tail = t; + indexInTail = 1; + size++; + } else { + tail[indexInTail] = o; + indexInTail++; + size++; + } + } + /** + * Returns the head buffer segment or null if the list is empty. + * @return + */ + public Object[] head() { + return head; + } + /** + * Returns the tail buffer segment or null if the list is empty. + * @return + */ + public Object[] tail() { + return tail; + } + /** + * Returns the total size of the list. + * @return + */ + public int size() { + return size; + } + /** + * Returns the index of the next slot in the tail buffer segment. + * @return + */ + public int indexInTail() { + return indexInTail; + } + /** + * Returns the capacity hint that indicates the capacity of each buffer segment. + * @return + */ + public int capacityHint() { + return capacityHint; + } + /* Test support */List toList() { + final int cap = capacityHint; + final int s = size; + final List list = new ArrayList(s + 1); + + Object[] h = head(); + int j = 0; + int k = 0; + while (j < s) { + list.add(h[k]); + j++; + if (++k == cap) { + k = 0; + h = (Object[])h[cap]; + } + } + + return list; + } + @Override + public String toString() { + return toList().toString(); + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/LinkedArrayListTest.java b/src/main/java/rx/internal/util/LinkedArrayListTest.java new file mode 100644 index 0000000000..1b7d34fa0b --- /dev/null +++ b/src/main/java/rx/internal/util/LinkedArrayListTest.java @@ -0,0 +1,37 @@ +/** + * 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.util; + +import java.util.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +public class LinkedArrayListTest { + @Test + public void testAdd() { + LinkedArrayList list = new LinkedArrayList(16); + + List expected = new ArrayList(32); + for (int i = 0; i < 32; i++) { + list.add(i); + expected.add(i); + } + + assertEquals(expected, list.toList()); + assertEquals(32, list.size()); + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/CachedObservableTest.java b/src/test/java/rx/internal/operators/CachedObservableTest.java new file mode 100644 index 0000000000..ec88045dcb --- /dev/null +++ b/src/test/java/rx/internal/operators/CachedObservableTest.java @@ -0,0 +1,293 @@ +/** + * 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 java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; + +import rx.*; +import rx.Observable.OnSubscribe; +import rx.Observable; +import rx.exceptions.TestException; +import rx.functions.*; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; + +public class CachedObservableTest { + @Test + public void testColdReplayNoBackpressure() { + CachedObservable source = CachedObservable.from(Observable.range(0, 1000)); + + assertFalse("Source is connected!", source.isConnected()); + + TestSubscriber ts = new TestSubscriber(); + + source.subscribe(ts); + + assertTrue("Source is not connected!", source.isConnected()); + assertFalse("Subscribers retained!", source.hasObservers()); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + List onNextEvents = ts.getOnNextEvents(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + @Test + public void testColdReplayBackpressure() { + CachedObservable source = CachedObservable.from(Observable.range(0, 1000)); + + assertFalse("Source is connected!", source.isConnected()); + + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(10); + + source.subscribe(ts); + + assertTrue("Source is not connected!", source.isConnected()); + assertTrue("Subscribers not retained!", source.hasObservers()); + + ts.assertNoErrors(); + assertTrue(ts.getOnCompletedEvents().isEmpty()); + List onNextEvents = ts.getOnNextEvents(); + assertEquals(10, onNextEvents.size()); + + for (int i = 0; i < 10; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + + ts.unsubscribe(); + assertFalse("Subscribers retained!", source.hasObservers()); + } + + @Test + public void testCache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable o = Observable.create(new Observable.OnSubscribe() { + + @Override + public void call(final Subscriber observer) { + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + observer.onNext("one"); + observer.onCompleted(); + } + }).start(); + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void testUnsubscribeSource() { + Action0 unsubscribe = mock(Action0.class); + Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, times(1)).call(); + } + + @Test + public void testTake() { + TestSubscriber ts = new TestSubscriber(); + + CachedObservable cached = CachedObservable.from(Observable.range(1, 100)); + cached.take(10).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + ts.assertUnsubscribed(); + assertFalse(cached.hasObservers()); + } + + @Test + public void testAsync() { + Observable source = Observable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestSubscriber ts1 = new TestSubscriber(); + + CachedObservable cached = CachedObservable.from(source); + + cached.observeOn(Schedulers.computation()).subscribe(ts1); + + ts1.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts1.assertNoErrors(); + ts1.assertTerminalEvent(); + assertEquals(10000, ts1.getOnNextEvents().size()); + + TestSubscriber ts2 = new TestSubscriber(); + cached.observeOn(Schedulers.computation()).subscribe(ts2); + + ts2.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts2.assertNoErrors(); + ts2.assertTerminalEvent(); + assertEquals(10000, ts2.getOnNextEvents().size()); + } + } + @Test + public void testAsyncComeAndGo() { + Observable source = Observable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + CachedObservable cached = CachedObservable.from(source); + + Observable output = cached.observeOn(Schedulers.computation()); + + List> list = new ArrayList>(100); + for (int i = 0; i < 100; i++) { + TestSubscriber ts = new TestSubscriber(); + list.add(ts); + output.skip(i * 10).take(10).subscribe(ts); + } + + List expected = new ArrayList(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestSubscriber ts : list) { + ts.awaitTerminalEvent(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + ts.assertReceivedOnNext(expected); + + j++; + } + } + + @Test + public void testNoMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Observable firehose = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onCompleted(); + } + }); + + TestSubscriber ts = new TestSubscriber(); + firehose.cache().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); + + ts.awaitTerminalEvent(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertEquals(100, ts.getOnNextEvents().size()); + } + + @Test + public void testValuesAndThenError() { + Observable source = Observable.range(1, 10) + .concatWith(Observable.error(new TestException())) + .cache(); + + + TestSubscriber ts = new TestSubscriber(); + source.subscribe(ts); + + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + Assert.assertEquals(1, ts.getOnErrorEvents().size()); + + TestSubscriber ts2 = new TestSubscriber(); + source.subscribe(ts2); + + ts2.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + Assert.assertTrue(ts2.getOnCompletedEvents().isEmpty()); + Assert.assertEquals(1, ts2.getOnErrorEvents().size()); + } + + @Test + public void unsafeChildThrows() { + final AtomicInteger count = new AtomicInteger(); + + Observable source = Observable.range(1, 100) + .doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }) + .cache(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.unsafeSubscribe(ts); + + Assert.assertEquals(100, count.get()); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java b/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java deleted file mode 100644 index 0d74cd878b..0000000000 --- a/src/test/java/rx/internal/operators/OnSubscribeCacheTest.java +++ /dev/null @@ -1,164 +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 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 java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import rx.Observable; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.functions.Func2; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.subjects.AsyncSubject; -import rx.subjects.BehaviorSubject; -import rx.subjects.PublishSubject; -import rx.subjects.ReplaySubject; -import rx.subjects.Subject; - -public class OnSubscribeCacheTest { - - @Test - public void testCache() throws InterruptedException { - final AtomicInteger counter = new AtomicInteger(); - Observable o = Observable.create(new Observable.OnSubscribe() { - - @Override - public void call(final Subscriber observer) { - new Thread(new Runnable() { - - @Override - public void run() { - counter.incrementAndGet(); - System.out.println("published observable being executed"); - observer.onNext("one"); - observer.onCompleted(); - } - }).start(); - } - }).cache(); - - // we then expect the following 2 subscriptions to get that same value - final CountDownLatch latch = new CountDownLatch(2); - - // subscribe once - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - // subscribe again - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - if (!latch.await(1000, TimeUnit.MILLISECONDS)) { - fail("subscriptions did not receive values"); - } - assertEquals(1, counter.get()); - } - - private void testWithCustomSubjectAndRepeat(Subject subject, Integer... expected) { - Observable source0 = Observable.just(1, 2, 3) - .subscribeOn(Schedulers.io()) - .flatMap(new Func1>() { - @Override - public Observable call(final Integer i) { - return Observable.timer(i * 20, TimeUnit.MILLISECONDS).map(new Func1() { - @Override - public Integer call(Long t1) { - return i; - } - }); - } - }); - - Observable source1 = Observable.create(new OnSubscribeCache(source0, subject)); - - Observable source2 = source1 - .repeat(4) - .zipWith(Observable.interval(0, 10, TimeUnit.MILLISECONDS, Schedulers.newThread()), new Func2() { - @Override - public Integer call(Integer t1, Long t2) { - return t1; - } - - }); - TestSubscriber ts = new TestSubscriber(); - source2.subscribe(ts); - - ts.awaitTerminalEvent(); - ts.assertNoErrors(); - System.out.println(ts.getOnNextEvents()); - ts.assertReceivedOnNext(Arrays.asList(expected)); - } - - @Test(timeout = 10000) - public void testWithAsyncSubjectAndRepeat() { - testWithCustomSubjectAndRepeat(AsyncSubject. create(), 3, 3, 3, 3); - } - - @Test(timeout = 10000) - public void testWithBehaviorSubjectAndRepeat() { - // BehaviorSubject just completes when repeated - testWithCustomSubjectAndRepeat(BehaviorSubject.create(0), 0, 1, 2, 3); - } - - @Test(timeout = 10000) - public void testWithPublishSubjectAndRepeat() { - // PublishSubject just completes when repeated - testWithCustomSubjectAndRepeat(PublishSubject. create(), 1, 2, 3); - } - - @Test - public void testWithReplaySubjectAndRepeat() { - testWithCustomSubjectAndRepeat(ReplaySubject. create(), 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3); - } - - @Test - public void testUnsubscribeSource() { - Action0 unsubscribe = mock(Action0.class); - Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); - o.subscribe(); - o.subscribe(); - o.subscribe(); - verify(unsubscribe, times(1)).call(); - } -} From 7d03daa8d6d0fb6d59d8f5561f0598d79cb85ea5 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 27 Jul 2015 08:41:02 +0200 Subject: [PATCH 349/857] Operator replay() now supports backpressure (again) --- src/main/java/rx/Observable.java | 213 ++- .../OnSubscribeMulticastSelector.java | 77 -- .../rx/internal/operators/OperatorReplay.java | 1192 ++++++++++++++++- .../operators/OperatorReplayTest.java | 399 +++++- 4 files changed, 1620 insertions(+), 261 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OnSubscribeMulticastSelector.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 9fd476eab7..8bdd016b58 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -25,7 +25,6 @@ import rx.observers.SafeSubscriber; import rx.plugins.*; import rx.schedulers.*; -import rx.subjects.*; import rx.subscriptions.Subscriptions; /** @@ -5987,9 +5986,9 @@ public Void call(Notification notification) { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -5999,14 +5998,7 @@ public Void call(Notification notification) { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay() { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject. create(); - } - - }); + return OperatorReplay.create(this); } /** @@ -6016,9 +6008,9 @@ public final ConnectableObservable replay() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -6033,12 +6025,12 @@ public final ConnectableObservable replay() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.create(); + public ConnectableObservable call() { + return Observable.this.replay(); } - }, selector)); + }, selector); } /** @@ -6049,9 +6041,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -6069,12 +6061,12 @@ public final Subject call() { * @see ReactiveX operators documentation: Replay */ public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.createWithSize(bufferSize); + public ConnectableObservable call() { + return Observable.this.replay(bufferSize); } - }, selector)); + }, selector); } /** @@ -6085,9 +6077,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6121,9 +6113,9 @@ public final Observable replay(Func1, ? extends Obs * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6153,12 +6145,12 @@ public final Observable replay(Func1, ? extends Obs if (bufferSize < 0) { throw new IllegalArgumentException("bufferSize < 0"); } - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.createWithTimeAndSize(time, unit, bufferSize, scheduler); + public ConnectableObservable call() { + return Observable.this.replay(bufferSize, time, unit, scheduler); } - }, selector)); + }, selector); } /** @@ -6169,9 +6161,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6190,13 +6182,18 @@ public final Subject call() { * replaying no more than {@code bufferSize} notifications * @see ReactiveX operators documentation: Replay */ - public final Observable replay(Func1, ? extends Observable> selector, final int bufferSize, final Scheduler scheduler) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + public final Observable replay(final Func1, ? extends Observable> selector, final int bufferSize, final Scheduler scheduler) { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return OperatorReplay. createScheduledSubject(ReplaySubject.createWithSize(bufferSize), scheduler); + public ConnectableObservable call() { + return Observable.this.replay(bufferSize); } - }, selector)); + }, new Func1, Observable>() { + @Override + public Observable call(Observable t) { + return selector.call(t).observeOn(scheduler); + } + }); } /** @@ -6207,9 +6204,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6240,9 +6237,9 @@ public final Observable replay(Func1, ? extends Obs * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6264,12 +6261,12 @@ 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 create(new OnSubscribeMulticastSelector(this, new Func0>() { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return ReplaySubject.createWithTime(time, unit, scheduler); + public ConnectableObservable call() { + return Observable.this.replay(time, unit, scheduler); } - }, selector)); + }, selector); } /** @@ -6279,9 +6276,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6298,13 +6295,18 @@ public final Subject call() { * replaying all items * @see ReactiveX operators documentation: Replay */ - public final Observable replay(Func1, ? extends Observable> selector, final Scheduler scheduler) { - return create(new OnSubscribeMulticastSelector(this, new Func0>() { + public final Observable replay(final Func1, ? extends Observable> selector, final Scheduler scheduler) { + return OperatorReplay.multicastSelector(new Func0>() { @Override - public final Subject call() { - return OperatorReplay.createScheduledSubject(ReplaySubject. create(), scheduler); + public ConnectableObservable call() { + return Observable.this.replay(); } - }, selector)); + }, new Func1, Observable>() { + @Override + public Observable call(Observable t) { + return selector.call(t).observeOn(scheduler); + } + }); } /** @@ -6316,9 +6318,9 @@ public final Subject call() { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} does not operate by default on a particular {@link Scheduler}.
*
@@ -6330,14 +6332,7 @@ public final Subject call() { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject.createWithSize(bufferSize); - } - - }); + return OperatorReplay.create(this, bufferSize); } /** @@ -6349,9 +6344,9 @@ public final ConnectableObservable replay(final int bufferSize) { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6380,9 +6375,9 @@ public final ConnectableObservable replay(int bufferSize, long time, TimeUnit * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6406,14 +6401,7 @@ public final ConnectableObservable replay(final int bufferSize, final long ti if (bufferSize < 0) { throw new IllegalArgumentException("bufferSize < 0"); } - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject.createWithTimeAndSize(time, unit, bufferSize, scheduler); - } - - }); + return OperatorReplay.create(this, time, unit, scheduler, bufferSize); } /** @@ -6425,9 +6413,9 @@ public final ConnectableObservable replay(final int bufferSize, final long ti * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6441,14 +6429,7 @@ public final ConnectableObservable replay(final int bufferSize, final long ti * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final int bufferSize, final Scheduler scheduler) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return OperatorReplay.createScheduledSubject(ReplaySubject.createWithSize(bufferSize), scheduler); - } - - }); + return OperatorReplay.observeOn(replay(bufferSize), scheduler); } /** @@ -6460,9 +6441,9 @@ public final ConnectableObservable replay(final int bufferSize, final Schedul * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.
*
@@ -6488,9 +6469,9 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6506,14 +6487,7 @@ public final ConnectableObservable replay(long time, TimeUnit unit) { * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final long time, final TimeUnit unit, final Scheduler scheduler) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return ReplaySubject.createWithTime(time, unit, scheduler); - } - - }); + return OperatorReplay.create(this, time, unit, scheduler); } /** @@ -6525,9 +6499,9 @@ public final ConnectableObservable replay(final long time, final TimeUnit uni * *
*
Backpressure Support:
- *
This operator does not support backpressure because multicasting means the stream is "hot" with - * multiple subscribers. Each child will need to manage backpressure independently using operators such - * as {@link #onBackpressureDrop} and {@link #onBackpressureBuffer}.
+ *
This operator supports backpressure. Note that the upstream requests are determined by the child + * Subscriber which requests the largest amount: i.e., two child Subscribers with requests of 10 and 100 will + * request 100 elements from the underlying Observable sequence.
*
Scheduler:
*
you specify which {@link Scheduler} this operator will use
*
@@ -6540,14 +6514,7 @@ public final ConnectableObservable replay(final long time, final TimeUnit uni * @see ReactiveX operators documentation: Replay */ public final ConnectableObservable replay(final Scheduler scheduler) { - return new OperatorMulticast(this, new Func0>() { - - @Override - public Subject call() { - return OperatorReplay.createScheduledSubject(ReplaySubject. create(), scheduler); - } - - }); + return OperatorReplay.observeOn(replay(), scheduler); } /** diff --git a/src/main/java/rx/internal/operators/OnSubscribeMulticastSelector.java b/src/main/java/rx/internal/operators/OnSubscribeMulticastSelector.java deleted file mode 100644 index d1457ca6ec..0000000000 --- a/src/main/java/rx/internal/operators/OnSubscribeMulticastSelector.java +++ /dev/null @@ -1,77 +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 rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action1; -import rx.functions.Func0; -import rx.functions.Func1; -import rx.observables.ConnectableObservable; -import rx.observers.SafeSubscriber; -import rx.subjects.Subject; - -/** - * Returns an observable sequence that contains the elements of a sequence - * produced by multicasting the source sequence within a selector function. - * - * @see MSDN: Observable.Multicast - * - * @param the input value type - * @param the intermediate type - * @param the result type - */ -public final class OnSubscribeMulticastSelector implements OnSubscribe { - final Observable source; - final Func0> subjectFactory; - final Func1, ? extends Observable> resultSelector; - - public OnSubscribeMulticastSelector(Observable source, - Func0> subjectFactory, - Func1, ? extends Observable> resultSelector) { - this.source = source; - this.subjectFactory = subjectFactory; - this.resultSelector = resultSelector; - } - - @Override - public void call(Subscriber child) { - Observable observable; - ConnectableObservable connectable; - try { - connectable = new OperatorMulticast(source, subjectFactory); - - observable = resultSelector.call(connectable); - } catch (Throwable t) { - child.onError(t); - return; - } - - final SafeSubscriber s = new SafeSubscriber(child); - - observable.unsafeSubscribe(s); - - connectable.connect(new Action1() { - @Override - public void call(Subscription t1) { - s.add(t1); - } - }); - } - -} diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index 83c76dfe39..e1bf7aa352 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -15,93 +15,1181 @@ */ package rx.internal.operators; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; +import rx.*; import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Scheduler; -import rx.Subscriber; -import rx.subjects.Subject; +import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorThrowable; +import rx.functions.*; +import rx.observables.ConnectableObservable; +import rx.schedulers.Timestamped; +import rx.subscriptions.Subscriptions; -/** - * Replay with limited buffer and/or time constraints. - * - * - * @see MSDN: Observable.Replay overloads - */ -public final class OperatorReplay { - /** Utility class. */ - private OperatorReplay() { - throw new IllegalStateException("No instances!"); +public final class OperatorReplay extends ConnectableObservable { + /** The source observable. */ + final Observable source; + /** Holds the current subscriber that is, will be or just was subscribed to the source observable. */ + final AtomicReference> current; + /** A factory that creates the appropriate buffer for the ReplaySubscriber. */ + final Func0> bufferFactory; + + @SuppressWarnings("rawtypes") + static final Func0 DEFAULT_UNBOUNDED_FACTORY = new Func0() { + @Override + public Object call() { + return new UnboundedReplayBuffer(16); + } + }; + + /** + * Given a connectable observable factory, it multicasts over the generated + * ConnectableObservable via a selector function. + * @param connectableFactory + * @param selector + * @return + */ + public static Observable multicastSelector( + final Func0> connectableFactory, + final Func1, ? extends Observable> selector) { + return Observable.create(new OnSubscribe() { + @Override + public void call(final Subscriber child) { + ConnectableObservable co; + Observable observable; + try { + co = connectableFactory.call(); + observable = selector.call(co); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + child.onError(e); + return; + } + + observable.subscribe(child); + + co.connect(new Action1() { + @Override + public void call(Subscription t) { + child.add(t); + } + }); + } + }); + } + + /** + * Child Subscribers will observe the events of the ConnectableObservable on the + * specified scheduler. + * @param co + * @param scheduler + * @return + */ + public static ConnectableObservable observeOn(final ConnectableObservable co, final Scheduler scheduler) { + final Observable observable = co.observeOn(scheduler); + OnSubscribe onSubscribe = new OnSubscribe() { + @Override + public void call(final Subscriber child) { + // apply observeOn and prevent calling onStart() again + observable.unsafeSubscribe(new Subscriber(child) { + @Override + public void onNext(T t) { + child.onNext(t); + } + @Override + public void onError(Throwable e) { + child.onError(e); + } + @Override + public void onCompleted() { + child.onCompleted(); + } + }); + } + }; + return new ConnectableObservable(onSubscribe) { + @Override + public void connect(Action1 connection) { + co.connect(connection); + } + }; + } + + /** + * Creates a replaying ConnectableObservable with an unbounded buffer. + * @param source + * @return + */ + @SuppressWarnings("unchecked") + public static ConnectableObservable create(Observable source) { + return create(source, DEFAULT_UNBOUNDED_FACTORY); + } + + /** + * Creates a replaying ConnectableObservable with a size bound buffer. + * @param source + * @param bufferSize + * @return + */ + public static ConnectableObservable create(Observable source, + final int bufferSize) { + if (bufferSize == Integer.MAX_VALUE) { + return create(source); + } + return create(source, new Func0>() { + @Override + public ReplayBuffer call() { + return new SizeBoundReplayBuffer(bufferSize); + } + }); } /** - * Creates a subject whose client observers will observe events - * propagated through the given wrapped subject. - * @param the element type - * @param subject the subject to wrap - * @param scheduler the target scheduler - * @return the created subject + * Creates a replaying ConnectableObservable with a time bound buffer. + * @param source + * @param maxAge + * @param unit + * @param scheduler + * @return */ - public static Subject createScheduledSubject(Subject subject, Scheduler scheduler) { - final Observable observedOn = subject.observeOn(scheduler); - SubjectWrapper s = new SubjectWrapper(new OnSubscribe() { + public static ConnectableObservable create(Observable source, + long maxAge, TimeUnit unit, Scheduler scheduler) { + return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE); + } + /** + * Creates a replaying ConnectableObservable with a size and time bound buffer. + * @param source + * @param maxAge + * @param unit + * @param scheduler + * @param bufferSize + * @return + */ + public static ConnectableObservable create(Observable source, + long maxAge, TimeUnit unit, final Scheduler scheduler, final int bufferSize) { + final long maxAgeInMillis = unit.toMillis(maxAge); + return create(source, new Func0>() { @Override - public void call(Subscriber o) { - subscriberOf(observedOn).call(o); + public ReplayBuffer call() { + return new SizeAndTimeBoundReplayBuffer(bufferSize, maxAgeInMillis, scheduler); } - - }, subject); - return s; + }); } /** - * Return an OnSubscribeFunc which delegates the subscription to the given observable. - * - * @param the value type - * @param target the target observable - * @return the function that delegates the subscription to the target + * Creates a OperatorReplay instance to replay values of the given source observable. + * @param source the source observable + * @param bufferFactory the factory to instantiate the appropriate buffer when the observable becomes active + * @return the connectable observable */ - public static OnSubscribe subscriberOf(final Observable target) { - return new OnSubscribe() { + static ConnectableObservable create(Observable source, + final Func0> bufferFactory) { + // the current connection to source needs to be shared between the operator and its onSubscribe call + final AtomicReference> curr = new AtomicReference>(); + OnSubscribe onSubscribe = new OnSubscribe() { @Override - public void call(Subscriber t1) { - target.unsafeSubscribe(t1); + public void call(Subscriber child) { + // concurrent connection/disconnection may change the state, + // we loop to be atomic while the child subscribes + for (;;) { + // get the current subscriber-to-source + ReplaySubscriber r = curr.get(); + // if there isn't one + if (r == null) { + // create a new subscriber to source + ReplaySubscriber u = new ReplaySubscriber(curr, bufferFactory.call()); + // perform extra initialization to avoid 'this' to escape during construction + u.init(); + // let's try setting it as the current subscriber-to-source + if (!curr.compareAndSet(r, u)) { + // didn't work, maybe someone else did it or the current subscriber + // to source has just finished + continue; + } + // we won, let's use it going onwards + r = u; + } + + // create the backpressure-managing producer for this child + InnerProducer inner = new InnerProducer(r, child); + // we try to add it to the array of producers + // if it fails, no worries because we will still have its buffer + // so it is going to replay it for us + r.add(inner); + // the producer has been registered with the current subscriber-to-source so + // at least it will receive the next terminal event + child.add(inner); + // setting the producer will trigger the first request to be considered by + // the subscriber-to-source. + child.setProducer(inner); + break; + } } }; + return new OperatorReplay(onSubscribe, source, curr, bufferFactory); + } + private OperatorReplay(OnSubscribe onSubscribe, Observable source, + final AtomicReference> current, + final Func0> bufferFactory) { + super(onSubscribe); + this.source = source; + this.current = current; + this.bufferFactory = bufferFactory; + } + + @Override + public void connect(Action1 connection) { + boolean doConnect = false; + ReplaySubscriber ps; + // we loop because concurrent connect/disconnect and termination may change the state + for (;;) { + // retrieve the current subscriber-to-source instance + ps = current.get(); + // if there is none yet or the current has unsubscribed + if (ps == null || ps.isUnsubscribed()) { + // create a new subscriber-to-source + ReplaySubscriber u = new ReplaySubscriber(current, bufferFactory.call()); + // initialize out the constructor to avoid 'this' to escape + u.init(); + // try setting it as the current subscriber-to-source + if (!current.compareAndSet(ps, u)) { + // did not work, perhaps a new subscriber arrived + // and created a new subscriber-to-source as well, retry + continue; + } + ps = u; + } + // if connect() was called concurrently, only one of them should actually + // connect to the source + doConnect = !ps.shouldConnect.get() && ps.shouldConnect.compareAndSet(false, true); + break; + } + /* + * Notify the callback that we have a (new) connection which it can unsubscribe + * but since ps is unique to a connection, multiple calls to connect() will return the + * same Subscription and even if there was a connect-disconnect-connect pair, the older + * references won't disconnect the newer connection. + * Synchronous source consumers have the opportunity to disconnect via unsubscribe on the + * Subscription as unsafeSubscribe may never return in its own. + * + * Note however, that asynchronously disconnecting a running source might leave + * child-subscribers without any terminal event; ReplaySubject does not have this + * issue because the unsubscription was always triggered by the child-subscribers + * themselves. + */ + connection.call(ps); + if (doConnect) { + source.unsafeSubscribe(ps); + } } + + @SuppressWarnings("rawtypes") + static final class ReplaySubscriber extends Subscriber implements Subscription { + /** Holds notifications from upstream. */ + final ReplayBuffer buffer; + /** The notification-lite factory. */ + final NotificationLite nl; + /** Contains either an onCompleted or an onError token from upstream. */ + boolean done; + + /** Indicates an empty array of inner producers. */ + static final InnerProducer[] EMPTY = new InnerProducer[0]; + /** Indicates a terminated ReplaySubscriber. */ + static final InnerProducer[] TERMINATED = new InnerProducer[0]; + + /** Tracks the subscribed producers. */ + final AtomicReference producers; + /** + * Atomically changed from false to true by connect to make sure the + * connection is only performed by one thread. + */ + final AtomicBoolean shouldConnect; + + /** Guarded by this. */ + boolean emitting; + /** Guarded by this. */ + boolean missed; + + + /** Contains the maximum element index the child Subscribers requested so far. Accessed while emitting is true. */ + long maxChildRequested; + /** Counts the outstanding upstream requests until the producer arrives. */ + long maxUpstreamRequested; + /** The upstream producer. */ + volatile Producer producer; + + public ReplaySubscriber(AtomicReference> current, + ReplayBuffer buffer) { + this.buffer = buffer; + + this.nl = NotificationLite.instance(); + this.producers = new AtomicReference(EMPTY); + this.shouldConnect = new AtomicBoolean(); + // make sure the source doesn't produce values until the child subscribers + // expressed their request amounts + this.request(0); + } + /** Should be called after the constructor finished to setup nulling-out the current reference. */ + void init() { + add(Subscriptions.create(new Action0() { + @Override + public void call() { + ReplaySubscriber.this.producers.getAndSet(TERMINATED); + // unlike OperatorPublish, we can't null out the terminated so + // late subscribers can still get replay + // current.compareAndSet(ReplaySubscriber.this, null); + // we don't care if it fails because it means the current has + // been replaced in the meantime + } + })); + } + /** + * Atomically try adding a new InnerProducer to this Subscriber or return false if this + * Subscriber was terminated. + * @param producer the producer to add + * @return true if succeeded, false otherwise + */ + boolean add(InnerProducer producer) { + if (producer == null) { + throw new NullPointerException(); + } + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // get the current producer array + InnerProducer[] c = producers.get(); + // if this subscriber-to-source reached a terminal state by receiving + // an onError or onCompleted, just refuse to add the new producer + if (c == TERMINATED) { + return false; + } + // we perform a copy-on-write logic + int len = c.length; + InnerProducer[] u = new InnerProducer[len + 1]; + System.arraycopy(c, 0, u, 0, len); + u[len] = producer; + // try setting the producers array + if (producers.compareAndSet(c, u)) { + return true; + } + // if failed, some other operation succeded (another add, remove or termination) + // so retry + } + } + + /** + * Atomically removes the given producer from the producers array. + * @param producer the producer to remove + */ + void remove(InnerProducer producer) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // let's read the current producers array + InnerProducer[] c = producers.get(); + // if it is either empty or terminated, there is nothing to remove so we quit + if (c == EMPTY || c == TERMINATED) { + return; + } + // let's find the supplied producer in the array + // although this is O(n), we don't expect too many child subscribers in general + int j = -1; + int len = c.length; + for (int i = 0; i < len; i++) { + if (c[i].equals(producer)) { + j = i; + break; + } + } + // we didn't find it so just quit + if (j < 0) { + return; + } + // we do copy-on-write logic here + InnerProducer[] u; + // we don't create a new empty array if producer was the single inhabitant + // but rather reuse an empty array + if (len == 1) { + u = EMPTY; + } else { + // otherwise, create a new array one less in size + u = new InnerProducer[len - 1]; + // copy elements being before the given producer + System.arraycopy(c, 0, u, 0, j); + // copy elements being after the given producer + System.arraycopy(c, j + 1, u, j, len - j - 1); + } + // try setting this new array as + if (producers.compareAndSet(c, u)) { + return; + } + // if we failed, it means something else happened + // (a concurrent add/remove or termination), we need to retry + } + } + + @Override + public void setProducer(Producer p) { + Producer p0 = producer; + if (p0 != null) { + throw new IllegalStateException("Only a single producer can be set on a Subscriber."); + } + producer = p; + manageRequests(); + replay(); + } + + @Override + public void onNext(T t) { + if (!done) { + buffer.next(t); + replay(); + } + } + @Override + public void onError(Throwable e) { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + try { + buffer.error(e); + replay(); + } finally { + unsubscribe(); // expectation of testIssue2191 + } + } + } + @Override + public void onCompleted() { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + try { + buffer.complete(); + replay(); + } finally { + unsubscribe(); + } + } + } + + /** + * Coordinates the request amounts of various child Subscribers. + */ + void manageRequests() { + // if the upstream has completed, no more requesting is possible + if (isUnsubscribed()) { + return; + } + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + } + for (;;) { + // if the upstream has completed, no more requesting is possible + if (isUnsubscribed()) { + return; + } + + @SuppressWarnings("unchecked") + InnerProducer[] a = producers.get(); + + long ri = maxChildRequested; + long maxTotalRequests = 0; + + for (InnerProducer rp : a) { + maxTotalRequests = Math.max(maxTotalRequests, rp.totalRequested.get()); + } + + long ur = maxUpstreamRequested; + Producer p = producer; + long diff = maxTotalRequests - ri; + if (diff != 0) { + maxChildRequested = maxTotalRequests; + if (p != null) { + if (ur != 0L) { + maxUpstreamRequested = 0L; + p.request(ur + diff); + } else { + p.request(diff); + } + } else { + // collect upstream request amounts until there is a producer for them + long u = ur + diff; + if (u < 0) { + u = Long.MAX_VALUE; + } + maxUpstreamRequested = u; + } + } else + // if there were outstanding upstream requests and we have a producer + if (ur != 0L && p != null) { + maxUpstreamRequested = 0L; + // fire the accumulated requests + p.request(ur); + } + + synchronized (this) { + if (!missed) { + emitting = false; + return; + } + missed = false; + } + } + } + + /** + * Tries to replay the buffer contents to all known subscribers. + */ + void replay() { + @SuppressWarnings("unchecked") + InnerProducer[] a = producers.get(); + for (InnerProducer rp : a) { + buffer.replay(rp); + } + } + } /** - * A subject that wraps another subject. + * A Producer and Subscription that manages the request and unsubscription state of a + * child subscriber in thread-safe manner. + * We use AtomicLong as a base class to save on extra allocation of an AtomicLong and also + * save the overhead of the AtomicIntegerFieldUpdater. * @param the value type */ - public static final class SubjectWrapper extends Subject { - /** The wrapped subject. */ - final Subject subject; + static final class InnerProducer extends AtomicLong implements Producer, Subscription { + /** */ + private static final long serialVersionUID = -4453897557930727610L; + /** + * The parent subscriber-to-source used to allow removing the child in case of + * child unsubscription. + */ + final ReplaySubscriber parent; + /** The actual child subscriber. */ + final Subscriber child; + /** + * Holds an object that represents the current location in the buffer. + * Guarded by the emitter loop. + */ + Object index; + /** + * Keeps the sum of all requested amounts. + */ + final AtomicLong totalRequested; + /** Indicates an emission state. Guarded by this. */ + boolean emitting; + /** Indicates a missed update. Guarded by this. */ + boolean missed; + /** + * Indicates this child has been unsubscribed: the state is swapped in atomically and + * will prevent the dispatch() to emit (too many) values to a terminated child subscriber. + */ + static final long UNSUBSCRIBED = Long.MIN_VALUE; + + public InnerProducer(ReplaySubscriber parent, Subscriber child) { + this.parent = parent; + this.child = child; + this.totalRequested = new AtomicLong(); + } + + @Override + public void request(long n) { + // ignore negative requests + if (n < 0) { + return; + } + // In general, RxJava doesn't prevent concurrent requests (with each other or with + // an unsubscribe) so we need a CAS-loop, but we need to handle + // request overflow and unsubscribed/not requested state as well. + for (;;) { + // get the current request amount + long r = get(); + // if child called unsubscribe() do nothing + if (r == UNSUBSCRIBED) { + return; + } + // ignore zero requests except any first that sets in zero + if (r >= 0L && n == 0) { + return; + } + // otherwise, increase the request count + long u = r + n; + // and check for long overflow + if (u < 0) { + // cap at max value, which is essentially unlimited + u = Long.MAX_VALUE; + } + // try setting the new request value + if (compareAndSet(r, u)) { + // increment the total request counter + addTotalRequested(n); + // if successful, notify the parent dispacher this child can receive more + // elements + parent.manageRequests(); + + parent.buffer.replay(this); + return; + } + // otherwise, someone else changed the state (perhaps a concurrent + // request or unsubscription so retry + } + } + + /** + * Increments the total requested amount. + * @param n the additional request amount + */ + void addTotalRequested(long n) { + for (;;) { + long r = totalRequested.get(); + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + if (totalRequested.compareAndSet(r, u)) { + return; + } + } + } + + /** + * Indicate that values have been emitted to this child subscriber by the dispatch() method. + * @param n the number of items emitted + * @return the updated request value (may indicate how much can be produced or a terminal state) + */ + public long produced(long n) { + // we don't allow producing zero or less: it would be a bug in the operator + if (n <= 0) { + throw new IllegalArgumentException("Cant produce zero or less"); + } + for (;;) { + // get the current request value + long r = get(); + // if the child has unsubscribed, simply return and indicate this + if (r == UNSUBSCRIBED) { + return UNSUBSCRIBED; + } + // reduce the requested amount + long u = r - n; + // if the new amount is less than zero, we have a bug in this operator + if (u < 0) { + throw new IllegalStateException("More produced (" + n + ") than requested (" + r + ")"); + } + // try updating the request value + if (compareAndSet(r, u)) { + // and return the udpated value + return u; + } + // otherwise, some concurrent activity happened and we need to retry + } + } + + @Override + public boolean isUnsubscribed() { + return get() == UNSUBSCRIBED; + } + @Override + public void unsubscribe() { + long r = get(); + // let's see if we are unsubscribed + if (r != UNSUBSCRIBED) { + // if not, swap in the terminal state, this is idempotent + // because other methods using CAS won't overwrite this value, + // concurrent calls to unsubscribe will atomically swap in the same + // terminal value + r = getAndSet(UNSUBSCRIBED); + // and only one of them will see a non-terminated value before the swap + if (r != UNSUBSCRIBED) { + // remove this from the parent + parent.remove(this); + // After removal, we might have unblocked the other child subscribers: + // let's assume this child had 0 requested before the unsubscription while + // the others had non-zero. By removing this 'blocking' child, the others + // are now free to receive events + parent.manageRequests(); + } + } + } + /** + * Convenience method to auto-cast the index object. + * @return + */ + @SuppressWarnings("unchecked") + U index() { + return (U)index; + } + } + /** + * The interface for interacting with various buffering logic. + * + * @param the value type + */ + interface ReplayBuffer { + /** + * Adds a regular value to the buffer. + * @param value + */ + void next(T value); + /** + * Adds a terminal exception to the buffer + * @param e + */ + void error(Throwable e); + /** + * Adds a completion event to the buffer + */ + void complete(); + /** + * Tries to replay the buffered values to the + * subscriber inside the output if there + * is new value and requests available at the + * same time. + * @param output + */ + void replay(InnerProducer output); + } + + /** + * Holds an unbounded list of events. + * + * @param the value type + */ + static final class UnboundedReplayBuffer extends ArrayList implements ReplayBuffer { + /** */ + private static final long serialVersionUID = 7063189396499112664L; + final NotificationLite nl; + /** The total number of events in the buffer. */ + volatile int size; + + public UnboundedReplayBuffer(int capacityHint) { + super(capacityHint); + nl = NotificationLite.instance(); + } + @Override + public void next(T value) { + add(nl.next(value)); + size++; + } - public SubjectWrapper(OnSubscribe func, Subject subject) { - super(func); - this.subject = subject; + @Override + public void error(Throwable e) { + add(nl.error(e)); + size++; } @Override - public void onNext(T args) { - subject.onNext(args); + public void complete() { + add(nl.completed()); + size++; } @Override - public void onError(Throwable e) { - subject.onError(e); + public void replay(InnerProducer output) { + synchronized (output) { + if (output.emitting) { + output.missed = true; + return; + } + output.emitting = true; + } + for (;;) { + if (output.isUnsubscribed()) { + return; + } + int sourceIndex = size; + + Integer destIndexObject = output.index(); + int destIndex = destIndexObject != null ? destIndexObject.intValue() : 0; + + long r = output.get(); + long r0 = r; + long e = 0L; + + while (r != 0L && destIndex < sourceIndex) { + Object o = get(destIndex); + try { + if (nl.accept(output.child, o)) { + return; + } + } catch (Throwable err) { + Exceptions.throwIfFatal(err); + output.unsubscribe(); + if (!nl.isError(o) && !nl.isCompleted(o)) { + output.child.onError(OnErrorThrowable.addValueAsLastCause(err, nl.getValue(o))); + } + return; + } + if (output.isUnsubscribed()) { + return; + } + destIndex++; + r--; + e++; + } + if (e != 0L) { + output.index = destIndex; + if (r0 != Long.MAX_VALUE) { + output.produced(e); + } + } + + synchronized (output) { + if (!output.missed) { + output.emitting = false; + return; + } + output.missed = false; + } + } + } + } + + /** + * Represents a node in a bounded replay buffer's linked list. + * + * @param the contained value type + */ + static final class Node extends AtomicReference { + /** */ + private static final long serialVersionUID = 245354315435971818L; + final Object value; + public Node(Object value) { + this.value = value; + } + } + + /** + * Base class for bounded buffering with options to specify an + * enter and leave transforms and custom truncation behavior. + * + * @param the value type + */ + static class BoundedReplayBuffer extends AtomicReference implements ReplayBuffer { + /** */ + private static final long serialVersionUID = 2346567790059478686L; + final NotificationLite nl; + + Node tail; + int size; + + public BoundedReplayBuffer() { + nl = NotificationLite.instance(); + Node n = new Node(null); + tail = n; + set(n); + } + + /** + * Add a new node to the linked list. + * @param n + */ + final void addLast(Node n) { + tail.set(n); + tail = n; + size++; + } + /** + * Remove the first node from the linked list. + */ + final void removeFirst() { + Node head = get(); + Node next = head.get(); + if (next == null) { + throw new IllegalStateException("Empty list!"); + } + size--; + // can't just move the head because it would retain the very first value + // can't null out the head's value because of late replayers would see null + setFirst(next); + } + /* test */ final void removeSome(int n) { + Node head = get(); + while (n > 0) { + head = head.get(); + n--; + size--; + } + + setFirst(head); + } + /** + * Arranges the given node is the new head from now on. + * @param n + */ + final void setFirst(Node n) { + set(n); + } + + @Override + public final void next(T value) { + Object o = enterTransform(nl.next(value)); + Node n = new Node(o); + addLast(n); + truncate(); } @Override - public void onCompleted() { - subject.onCompleted(); + public final void error(Throwable e) { + Object o = enterTransform(nl.error(e)); + Node n = new Node(o); + addLast(n); + truncateFinal(); } @Override - public boolean hasObservers() { - return this.subject.hasObservers(); + public final void complete() { + Object o = enterTransform(nl.completed()); + Node n = new Node(o); + addLast(n); + truncateFinal(); + } + + @Override + public final void replay(InnerProducer output) { + synchronized (output) { + if (output.emitting) { + output.missed = true; + return; + } + output.emitting = true; + } + for (;;) { + if (output.isUnsubscribed()) { + return; + } + + long r = output.get(); + long r0 = r; + long e = 0L; + + Node node = output.index(); + if (node == null) { + node = get(); + output.index = node; + } + + while (r != 0) { + Node v = node.get(); + if (v != null) { + Object o = leaveTransform(v.value); + try { + if (nl.accept(output.child, o)) { + output.index = null; + return; + } + } catch (Throwable err) { + output.index = null; + Exceptions.throwIfFatal(err); + output.unsubscribe(); + if (!nl.isError(o) && !nl.isCompleted(o)) { + output.child.onError(OnErrorThrowable.addValueAsLastCause(err, nl.getValue(o))); + } + return; + } + e++; + node = v; + } else { + break; + } + if (output.isUnsubscribed()) { + return; + } + } + + if (e != 0L) { + output.index = node; + if (r0 != Long.MAX_VALUE) { + output.produced(e); + } + } + + synchronized (output) { + if (!output.missed) { + output.emitting = false; + return; + } + output.missed = false; + } + } + + } + + /** + * Override this to wrap the NotificationLite object into a + * container to be used later by truncate. + * @param value + * @return + */ + Object enterTransform(Object value) { + return value; + } + /** + * Override this to unwrap the transformed value into a + * NotificationLite object. + * @param value + * @return + */ + Object leaveTransform(Object value) { + return value; + } + /** + * Override this method to truncate a non-terminated buffer + * based on its current properties. + */ + void truncate() { + + } + /** + * Override this method to truncate a terminated buffer + * based on its properties (i.e., truncate but the very last node). + */ + void truncateFinal() { + + } + /* test */ final void collect(Collection output) { + Node n = get(); + for (;;) { + Node next = n.get(); + if (next != null) { + Object o = next.value; + Object v = leaveTransform(o); + if (nl.isCompleted(v) || nl.isError(v)) { + break; + } + output.add(nl.getValue(v)); + n = next; + } else { + break; + } + } + } + /* test */ boolean hasError() { + return tail.value != null && nl.isError(leaveTransform(tail.value)); + } + /* test */ boolean hasCompleted() { + return tail.value != null && nl.isCompleted(leaveTransform(tail.value)); + } + } + + /** + * A bounded replay buffer implementation with size limit only. + * + * @param the value type + */ + static final class SizeBoundReplayBuffer extends BoundedReplayBuffer { + /** */ + private static final long serialVersionUID = -5898283885385201806L; + + final int limit; + public SizeBoundReplayBuffer(int limit) { + this.limit = limit; + } + + @Override + void truncate() { + // overflow can be at most one element + if (size > limit) { + removeFirst(); + } + } + + // no need for final truncation because values are truncated one by one + } + + /** + * Size and time bound replay buffer. + * + * @param the buffered value type + */ + static final class SizeAndTimeBoundReplayBuffer extends BoundedReplayBuffer { + /** */ + private static final long serialVersionUID = 3457957419649567404L; + final Scheduler scheduler; + final long maxAgeInMillis; + final int limit; + public SizeAndTimeBoundReplayBuffer(int limit, long maxAgeInMillis, Scheduler scheduler) { + this.scheduler = scheduler; + this.limit = limit; + this.maxAgeInMillis = maxAgeInMillis; + } + + @Override + Object enterTransform(Object value) { + return new Timestamped(scheduler.now(), value); + } + + @Override + Object leaveTransform(Object value) { + return ((Timestamped)value).getValue(); + } + + @Override + void truncate() { + long timeLimit = scheduler.now() - maxAgeInMillis; + + Node prev = get(); + Node next = prev.get(); + + int e = 0; + for (;;) { + if (next != null) { + if (size > limit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + Timestamped v = (Timestamped)next.value; + if (v.getTimestampMillis() <= timeLimit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + break; + } + } + } else { + break; + } + } + if (e != 0) { + setFirst(prev); + } + } + @Override + void truncateFinal() { + long timeLimit = scheduler.now() - maxAgeInMillis; + + Node prev = get(); + Node next = prev.get(); + + int e = 0; + for (;;) { + if (next != null && size > 1) { + Timestamped v = (Timestamped)next.value; + if (v.getTimestampMillis() <= timeLimit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + break; + } + } else { + break; + } + } + if (e != 0) { + setFirst(prev); + } } } } \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index a5ff85864d..046803b082 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -15,33 +15,40 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.notNull; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.Assert; import org.junit.Test; import org.mockito.InOrder; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.Observer; import rx.Scheduler; import rx.Scheduler.Worker; +import rx.Subscriber; import rx.Subscription; +import rx.exceptions.TestException; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; +import rx.internal.operators.OperatorReplay.BoundedReplayBuffer; +import rx.internal.operators.OperatorReplay.Node; +import rx.internal.operators.OperatorReplay.SizeAndTimeBoundReplayBuffer; import rx.observables.ConnectableObservable; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; @@ -739,4 +746,378 @@ public boolean isUnsubscribed() { } } + @Test + public void testBoundedReplayBuffer() { + BoundedReplayBuffer buf = new BoundedReplayBuffer(); + buf.addLast(new Node(1)); + buf.addLast(new Node(2)); + buf.addLast(new Node(3)); + buf.addLast(new Node(4)); + buf.addLast(new Node(5)); + + List values = new ArrayList(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5)); + buf.addLast(new Node(6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + + } + + @Test + public void testTimedAndSizedTruncation() { + TestScheduler test = Schedulers.test(); + SizeAndTimeBoundReplayBuffer buf = new SizeAndTimeBoundReplayBuffer(2, 2000, test); + List values = new ArrayList(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + } + + @Test + public void testBackpressure() { + final AtomicLong requested = new AtomicLong(); + Observable source = Observable.range(1, 1000) + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requested.addAndGet(t); + } + }); + ConnectableObservable co = source.replay(); + + TestSubscriber ts1 = TestSubscriber.create(10); + TestSubscriber ts2 = TestSubscriber.create(90); + + co.subscribe(ts1); + co.subscribe(ts2); + + ts2.requestMore(10); + + co.connect(); + + ts1.assertValueCount(10); + ts1.assertNoTerminalEvent(); + + ts2.assertValueCount(100); + ts2.assertNoTerminalEvent(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void testBackpressureBounded() { + final AtomicLong requested = new AtomicLong(); + Observable source = Observable.range(1, 1000) + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requested.addAndGet(t); + } + }); + ConnectableObservable co = source.replay(50); + + TestSubscriber ts1 = TestSubscriber.create(10); + TestSubscriber ts2 = TestSubscriber.create(90); + + co.subscribe(ts1); + co.subscribe(ts2); + + ts2.requestMore(10); + + co.connect(); + + ts1.assertValueCount(10); + ts1.assertNoTerminalEvent(); + + ts2.assertValueCount(100); + ts2.assertNoTerminalEvent(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void testColdReplayNoBackpressure() { + Observable source = Observable.range(0, 1000).replay().autoConnect(); + + TestSubscriber ts = new TestSubscriber(); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + List onNextEvents = ts.getOnNextEvents(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + @Test + public void testColdReplayBackpressure() { + Observable source = Observable.range(0, 1000).replay().autoConnect(); + + TestSubscriber ts = new TestSubscriber(); + ts.requestMore(10); + + source.subscribe(ts); + + ts.assertNoErrors(); + assertTrue(ts.getOnCompletedEvents().isEmpty()); + List onNextEvents = ts.getOnNextEvents(); + assertEquals(10, onNextEvents.size()); + + for (int i = 0; i < 10; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + + ts.unsubscribe(); + } + + @Test + public void testCache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable o = Observable.create(new Observable.OnSubscribe() { + + @Override + public void call(final Subscriber observer) { + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + observer.onNext("one"); + observer.onCompleted(); + } + }).start(); + } + }).replay().autoConnect(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void testUnsubscribeSource() { + Action0 unsubscribe = mock(Action0.class); + Observable o = Observable.just(1).doOnUnsubscribe(unsubscribe).cache(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, times(1)).call(); + } + + @Test + public void testTake() { + TestSubscriber ts = new TestSubscriber(); + + Observable cached = Observable.range(1, 100).replay().autoConnect(); + cached.take(10).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + ts.assertUnsubscribed(); + } + + @Test + public void testAsync() { + Observable source = Observable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestSubscriber ts1 = new TestSubscriber(); + + Observable cached = source.replay().autoConnect(); + + cached.observeOn(Schedulers.computation()).subscribe(ts1); + + ts1.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts1.assertNoErrors(); + ts1.assertTerminalEvent(); + assertEquals(10000, ts1.getOnNextEvents().size()); + + TestSubscriber ts2 = new TestSubscriber(); + cached.observeOn(Schedulers.computation()).subscribe(ts2); + + ts2.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts2.assertNoErrors(); + ts2.assertTerminalEvent(); + assertEquals(10000, ts2.getOnNextEvents().size()); + } + } + @Test + public void testAsyncComeAndGo() { + Observable source = Observable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + Observable cached = source.replay().autoConnect(); + + Observable output = cached.observeOn(Schedulers.computation()); + + List> list = new ArrayList>(100); + for (int i = 0; i < 100; i++) { + TestSubscriber ts = new TestSubscriber(); + list.add(ts); + output.skip(i * 10).take(10).subscribe(ts); + } + + List expected = new ArrayList(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestSubscriber ts : list) { + ts.awaitTerminalEvent(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + ts.assertReceivedOnNext(expected); + + j++; + } + } + + @Test + public void testNoMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Observable firehose = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onCompleted(); + } + }); + + TestSubscriber ts = new TestSubscriber(); + firehose.replay().autoConnect().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); + + ts.awaitTerminalEvent(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertEquals(100, ts.getOnNextEvents().size()); + } + + @Test + public void testValuesAndThenError() { + Observable source = Observable.range(1, 10) + .concatWith(Observable.error(new TestException())) + .replay().autoConnect(); + + + TestSubscriber ts = new TestSubscriber(); + source.subscribe(ts); + + ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + Assert.assertTrue(ts.getOnCompletedEvents().isEmpty()); + Assert.assertEquals(1, ts.getOnErrorEvents().size()); + + TestSubscriber ts2 = new TestSubscriber(); + source.subscribe(ts2); + + ts2.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + Assert.assertTrue(ts2.getOnCompletedEvents().isEmpty()); + Assert.assertEquals(1, ts2.getOnErrorEvents().size()); + } + + @Test + public void unsafeChildThrows() { + final AtomicInteger count = new AtomicInteger(); + + Observable source = Observable.range(1, 100) + .doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }) + .replay().autoConnect(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.unsafeSubscribe(ts); + + Assert.assertEquals(100, count.get()); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } } \ No newline at end of file From b4a759bac0bcffe437e24c46058a5043f0fc1a21 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 27 Jul 2015 08:42:25 +0200 Subject: [PATCH 350/857] Movet LinkedArrayListTest to the test section. --- src/{main => test}/java/rx/internal/util/LinkedArrayListTest.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{main => test}/java/rx/internal/util/LinkedArrayListTest.java (100%) diff --git a/src/main/java/rx/internal/util/LinkedArrayListTest.java b/src/test/java/rx/internal/util/LinkedArrayListTest.java similarity index 100% rename from src/main/java/rx/internal/util/LinkedArrayListTest.java rename to src/test/java/rx/internal/util/LinkedArrayListTest.java From 2423a17b5c3e5917a1960ffea8f5ecb11ac373a6 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 27 Jul 2015 10:16:26 +0200 Subject: [PATCH 351/857] Test coverage for the observers package. --- .../java/rx/observers/SerializedObserver.java | 188 +++----- src/main/java/rx/observers/TestObserver.java | 14 +- .../java/rx/observers/TestSubscriber.java | 13 +- src/test/java/rx/observers/ObserversTest.java | 189 ++++++++ .../java/rx/observers/SafeObserverTest.java | 51 +- .../java/rx/observers/SafeSubscriberTest.java | 230 +++++++++ .../rx/observers/SerializedObserverTest.java | 199 ++++++-- .../java/rx/observers/SubscribersTest.java | 188 ++++++++ .../java/rx/observers/TestObserverTest.java | 117 ++++- .../java/rx/observers/TestSubscriberTest.java | 455 +++++++++++++++++- 10 files changed, 1475 insertions(+), 169 deletions(-) create mode 100644 src/test/java/rx/observers/ObserversTest.java create mode 100644 src/test/java/rx/observers/SafeSubscriberTest.java create mode 100644 src/test/java/rx/observers/SubscribersTest.java diff --git a/src/main/java/rx/observers/SerializedObserver.java b/src/main/java/rx/observers/SerializedObserver.java index 86ca42f8cf..8125ce54e6 100644 --- a/src/main/java/rx/observers/SerializedObserver.java +++ b/src/main/java/rx/observers/SerializedObserver.java @@ -16,7 +16,8 @@ package rx.observers; import rx.Observer; -import rx.exceptions.Exceptions; +import rx.exceptions.*; +import rx.internal.operators.NotificationLite; /** * Enforces single-threaded, serialized, ordered execution of {@link #onNext}, {@link #onCompleted}, and @@ -35,13 +36,15 @@ public class SerializedObserver implements Observer { private final Observer actual; - private boolean emitting = false; - private boolean terminated = false; + private boolean emitting; + /** Set to true if a terminal event was received. */ + private volatile boolean terminated; + /** If not null, it indicates more work. */ private FastList queue; + private final NotificationLite nl = NotificationLite.instance(); - private static final int MAX_DRAIN_ITERATION = Integer.MAX_VALUE; - private static final Object NULL_SENTINEL = new Object(); - private static final Object COMPLETE_SENTINEL = new Object(); + /** Number of iterations without additional safepoint poll in the drain loop. */ + private static final int MAX_DRAIN_ITERATION = 1024; static final class FastList { Object[] array; @@ -64,150 +67,119 @@ public void add(Object o) { } } - private static final class ErrorSentinel { - final Throwable e; - - ErrorSentinel(Throwable e) { - this.e = e; - } - } - public SerializedObserver(Observer s) { this.actual = s; } @Override - public void onCompleted() { - FastList list; + public void onNext(T t) { + if (terminated) { + return; + } synchronized (this) { if (terminated) { return; } - terminated = true; if (emitting) { - if (queue == null) { - queue = new FastList(); + FastList list = queue; + if (list == null) { + list = new FastList(); + queue = list; } - queue.add(COMPLETE_SENTINEL); + list.add(nl.next(t)); return; } emitting = true; - list = queue; - queue = null; } - drainQueue(list); - actual.onCompleted(); + try { + actual.onNext(t); + } catch (Throwable e) { + terminated = true; + Exceptions.throwIfFatal(e); + actual.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + return; + } + for (;;) { + for (int i = 0; i < MAX_DRAIN_ITERATION; i++) { + FastList list; + synchronized (this) { + list = queue; + if (list == null) { + emitting = false; + return; + } + queue = null; + } + for (Object o : list.array) { + if (o == null) { + break; + } + try { + if (nl.accept(actual, o)) { + terminated = true; + return; + } + } catch (Throwable e) { + terminated = true; + Exceptions.throwIfFatal(e); + actual.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + return; + } + } + } + } } - + @Override public void onError(final Throwable e) { Exceptions.throwIfFatal(e); - FastList list; + if (terminated) { + return; + } synchronized (this) { if (terminated) { return; } + terminated = true; if (emitting) { - if (queue == null) { - queue = new FastList(); + /* + * FIXME: generally, errors jump the queue but this wasn't true + * for SerializedObserver and may break existing expectations. + */ + FastList list = queue; + if (list == null) { + list = new FastList(); + queue = list; } - queue.add(new ErrorSentinel(e)); + list.add(nl.error(e)); return; } emitting = true; - list = queue; - queue = null; } - drainQueue(list); actual.onError(e); - synchronized(this) { - emitting = false; - } } @Override - public void onNext(T t) { - FastList list; - + public void onCompleted() { + if (terminated) { + return; + } synchronized (this) { if (terminated) { return; } + terminated = true; if (emitting) { - if (queue == null) { - queue = new FastList(); + FastList list = queue; + if (list == null) { + list = new FastList(); + queue = list; } - queue.add(t != null ? t : NULL_SENTINEL); - // another thread is emitting so we add to the queue and return + list.add(nl.completed()); return; } - // we can emit emitting = true; - // reference to the list to drain before emitting our value - list = queue; - queue = null; - } - - // we only get here if we won the right to emit, otherwise we returned in the if(emitting) block above - boolean skipFinal = false; - try { - int iter = MAX_DRAIN_ITERATION; - do { - drainQueue(list); - if (iter == MAX_DRAIN_ITERATION) { - // after the first draining we emit our own value - actual.onNext(t); - } - --iter; - if (iter > 0) { - synchronized (this) { - list = queue; - queue = null; - if (list == null) { - emitting = false; - skipFinal = true; - return; - } - } - } - } while (iter > 0); - } finally { - if (!skipFinal) { - synchronized (this) { - if (terminated) { - list = queue; - queue = null; - } else { - emitting = false; - list = null; - } - } - } - } - - // this will only drain if terminated (done here outside of synchronized block) - drainQueue(list); - } - - void drainQueue(FastList list) { - if (list == null || list.size == 0) { - return; - } - for (Object v : list.array) { - if (v == null) { - break; - } - if (v == NULL_SENTINEL) { - actual.onNext(null); - } else if (v == COMPLETE_SENTINEL) { - actual.onCompleted(); - } else if (v.getClass() == ErrorSentinel.class) { - actual.onError(((ErrorSentinel) v).e); - } else { - @SuppressWarnings("unchecked") - T t = (T)v; - actual.onNext(t); - } } + actual.onCompleted(); } } diff --git a/src/main/java/rx/observers/TestObserver.java b/src/main/java/rx/observers/TestObserver.java index e7f02131b6..c20784187a 100644 --- a/src/main/java/rx/observers/TestObserver.java +++ b/src/main/java/rx/observers/TestObserver.java @@ -117,13 +117,17 @@ public void assertReceivedOnNext(List items) { } for (int i = 0; i < items.size(); i++) { - if (items.get(i) == null) { + T expected = items.get(i); + T actual = onNextEvents.get(i); + if (expected == null) { // check for null equality - if (onNextEvents.get(i) != null) { - throw new AssertionError("Value at index: " + i + " expected to be [null] but was: [" + onNextEvents.get(i) + "]"); + if (actual != null) { + throw new AssertionError("Value at index: " + i + " expected to be [null] but was: [" + actual + "]"); } - } else if (!items.get(i).equals(onNextEvents.get(i))) { - throw new AssertionError("Value at index: " + i + " expected to be [" + items.get(i) + "] (" + items.get(i).getClass().getSimpleName() + ") but was: [" + onNextEvents.get(i) + "] (" + onNextEvents.get(i).getClass().getSimpleName() + ")"); + } else if (!expected.equals(actual)) { + throw new AssertionError("Value at index: " + i + + " expected to be [" + expected + "] (" + expected.getClass().getSimpleName() + + ") but was: [" + actual + "] (" + (actual != null ? actual.getClass().getSimpleName() : "null") + ")"); } } diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index a2255cf401..2d46a25179 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -258,10 +258,15 @@ public void assertUnsubscribed() { * if this {@code Subscriber} has received one or more {@code onError} notifications */ public void assertNoErrors() { - if (getOnErrorEvents().size() > 0) { - // can't use AssertionError because (message, cause) doesn't exist until Java 7 - throw new RuntimeException("Unexpected onError events: " + getOnErrorEvents().size(), getOnErrorEvents().get(0)); - // TODO possibly check for Java7+ and then use AssertionError at runtime (since we always compile with 7) + List onErrorEvents = getOnErrorEvents(); + if (onErrorEvents.size() > 0) { + AssertionError ae = new AssertionError("Unexpected onError events: " + getOnErrorEvents().size()); + if (onErrorEvents.size() == 1) { + ae.initCause(getOnErrorEvents().get(0)); + } else { + ae.initCause(new CompositeException(onErrorEvents)); + } + throw ae; } } diff --git a/src/test/java/rx/observers/ObserversTest.java b/src/test/java/rx/observers/ObserversTest.java new file mode 100644 index 0000000000..df8b3aae99 --- /dev/null +++ b/src/test/java/rx/observers/ObserversTest.java @@ -0,0 +1,189 @@ +/** + * 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.observers; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import rx.exceptions.*; +import rx.functions.*; + +public class ObserversTest { + @Test + public void testNotInstantiable() { + try { + Constructor c = Observers.class.getDeclaredConstructor(); + c.setAccessible(true); + Object instance = c.newInstance(); + fail("Could instantiate Actions! " + instance); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + } catch (InvocationTargetException ex) { + ex.printStackTrace(); + } catch (InstantiationException ex) { + ex.printStackTrace(); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + } + + @Test + public void testEmptyOnErrorNotImplemented() { + try { + Observers.empty().onError(new TestException()); + fail("OnErrorNotImplementedException not thrown!"); + } catch (OnErrorNotImplementedException ex) { + if (!(ex.getCause() instanceof TestException)) { + fail("TestException not wrapped, instead: " + ex.getCause()); + } + } + } + @Test + public void testCreate1OnErrorNotImplemented() { + try { + Observers.create(Actions.empty()).onError(new TestException()); + fail("OnErrorNotImplementedException not thrown!"); + } catch (OnErrorNotImplementedException ex) { + if (!(ex.getCause() instanceof TestException)) { + fail("TestException not wrapped, instead: " + ex.getCause()); + } + } + } + @Test(expected = IllegalArgumentException.class) + public void testCreate1Null() { + Observers.create(null); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate2Null() { + Action1 throwAction = Actions.empty(); + Observers.create(null, throwAction); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate3Null() { + Observers.create(Actions.empty(), null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreate4Null() { + Action1 throwAction = Actions.empty(); + Observers.create(null, throwAction, Actions.empty()); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate5Null() { + Observers.create(Actions.empty(), null, Actions.empty()); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate6Null() { + Action1 throwAction = Actions.empty(); + Observers.create(Actions.empty(), throwAction, null); + } + + @Test + public void testCreate1Value() { + final AtomicInteger value = new AtomicInteger(); + Action1 action = new Action1() { + @Override + public void call(Integer t) { + value.set(t); + } + }; + Observers.create(action).onNext(1); + + assertEquals(1, value.get()); + } + @Test + public void testCreate2Value() { + final AtomicInteger value = new AtomicInteger(); + Action1 action = new Action1() { + @Override + public void call(Integer t) { + value.set(t); + } + }; + Action1 throwAction = Actions.empty(); + Observers.create(action, throwAction).onNext(1); + + assertEquals(1, value.get()); + } + + @Test + public void testCreate3Value() { + final AtomicInteger value = new AtomicInteger(); + Action1 action = new Action1() { + @Override + public void call(Integer t) { + value.set(t); + } + }; + Action1 throwAction = Actions.empty(); + Observers.create(action, throwAction, Actions.empty()).onNext(1); + + assertEquals(1, value.get()); + } + + @Test + public void testError2() { + final AtomicReference value = new AtomicReference(); + Action1 action = new Action1() { + @Override + public void call(Throwable t) { + value.set(t); + } + }; + TestException exception = new TestException(); + Observers.create(Actions.empty(), action).onError(exception); + + assertEquals(exception, value.get()); + } + + @Test + public void testError3() { + final AtomicReference value = new AtomicReference(); + Action1 action = new Action1() { + @Override + public void call(Throwable t) { + value.set(t); + } + }; + TestException exception = new TestException(); + Observers.create(Actions.empty(), action, Actions.empty()).onError(exception); + + assertEquals(exception, value.get()); + } + + @Test + public void testCompleted() { + Action0 action = mock(Action0.class); + + Action1 throwAction = Actions.empty(); + Observers.create(Actions.empty(), throwAction, action).onCompleted(); + + verify(action).call(); + } + + @Test + public void testEmptyCompleted() { + Observers.create(Actions.empty()).onCompleted(); + + Action1 throwAction = Actions.empty(); + Observers.create(Actions.empty(), throwAction).onCompleted(); + } +} diff --git a/src/test/java/rx/observers/SafeObserverTest.java b/src/test/java/rx/observers/SafeObserverTest.java index 584c6ee117..1083e995c7 100644 --- a/src/test/java/rx/observers/SafeObserverTest.java +++ b/src/test/java/rx/observers/SafeObserverTest.java @@ -15,11 +15,7 @@ */ package rx.observers; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -27,9 +23,7 @@ import org.junit.Test; import rx.Subscriber; -import rx.exceptions.CompositeException; -import rx.exceptions.OnErrorFailedException; -import rx.exceptions.OnErrorNotImplementedException; +import rx.exceptions.*; import rx.functions.Action0; import rx.subscriptions.Subscriptions; @@ -462,4 +456,45 @@ public SafeObserverTestException(String message) { super(message); } } + + @Test + public void testOnCompletedThrows() { + final AtomicReference error = new AtomicReference(); + SafeSubscriber s = new SafeSubscriber(new Subscriber() { + @Override + public void onNext(Integer t) { + + } + @Override + public void onError(Throwable e) { + error.set(e); + } + @Override + public void onCompleted() { + throw new TestException(); + } + }); + + s.onCompleted(); + + assertTrue("Error not received", error.get() instanceof TestException); + } + + @Test + public void testActual() { + Subscriber actual = new Subscriber() { + @Override + public void onNext(Integer t) { + } + @Override + public void onError(Throwable e) { + } + @Override + public void onCompleted() { + } + }; + SafeSubscriber s = new SafeSubscriber(actual); + + assertSame(actual, s.getActual()); + } } diff --git a/src/test/java/rx/observers/SafeSubscriberTest.java b/src/test/java/rx/observers/SafeSubscriberTest.java new file mode 100644 index 0000000000..85c2d7b07f --- /dev/null +++ b/src/test/java/rx/observers/SafeSubscriberTest.java @@ -0,0 +1,230 @@ +/** + * 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.observers; + +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.*; + +import rx.exceptions.*; +import rx.functions.Action0; +import rx.plugins.*; +import rx.subscriptions.Subscriptions; + +public class SafeSubscriberTest { + + @Before + @After + public void resetBefore() { + RxJavaPlugins ps = RxJavaPlugins.getInstance(); + + try { + Method m = ps.getClass().getDeclaredMethod("reset"); + m.setAccessible(true); + m.invoke(ps); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + + @Test + public void testOnCompletedThrows() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onCompleted() { + throw new TestException(); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + + safe.onCompleted(); + + assertTrue(safe.isUnsubscribed()); + } + + @Test + public void testOnCompletedThrows2() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onCompleted() { + throw new OnErrorNotImplementedException(new TestException()); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + + try { + safe.onCompleted(); + } catch (OnErrorNotImplementedException ex) { + // expected + } + + assertTrue(safe.isUnsubscribed()); + } + + @Test + public void testPluginException() { + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + @Override + public void handleError(Throwable e) { + throw new RuntimeException(); + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onCompleted() { + throw new TestException(); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + + safe.onCompleted(); + } + + @Test(expected = OnErrorFailedException.class) + public void testPluginExceptionWhileOnErrorUnsubscribeThrows() { + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + int calls; + @Override + public void handleError(Throwable e) { + if (++calls == 2) { + throw new RuntimeException(); + } + } + }); + + TestSubscriber ts = new TestSubscriber(); + SafeSubscriber safe = new SafeSubscriber(ts); + safe.add(Subscriptions.create(new Action0() { + @Override + public void call() { + throw new RuntimeException(); + } + })); + + safe.onError(new TestException()); + } + + @Test(expected = RuntimeException.class) + public void testPluginExceptionWhileOnErrorThrowsNotImplAndUnsubscribeThrows() { + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + int calls; + @Override + public void handleError(Throwable e) { + if (++calls == 2) { + throw new RuntimeException(); + } + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onError(Throwable e) { + throw new OnErrorNotImplementedException(e); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + safe.add(Subscriptions.create(new Action0() { + @Override + public void call() { + throw new RuntimeException(); + } + })); + + safe.onError(new TestException()); + } + + @Test(expected = OnErrorFailedException.class) + public void testPluginExceptionWhileOnErrorThrows() { + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + int calls; + @Override + public void handleError(Throwable e) { + if (++calls == 2) { + throw new RuntimeException(); + } + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + + safe.onError(new TestException()); + } + @Test(expected = OnErrorFailedException.class) + public void testPluginExceptionWhileOnErrorThrowsAndUnsubscribeThrows() { + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + int calls; + @Override + public void handleError(Throwable e) { + if (++calls == 2) { + throw new RuntimeException(); + } + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + safe.add(Subscriptions.create(new Action0() { + @Override + public void call() { + throw new RuntimeException(); + } + })); + + safe.onError(new TestException()); + } + @Test(expected = OnErrorFailedException.class) + public void testPluginExceptionWhenUnsubscribing2() { + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + int calls; + @Override + public void handleError(Throwable e) { + if (++calls == 3) { + throw new RuntimeException(); + } + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + safe.add(Subscriptions.create(new Action0() { + @Override + public void call() { + throw new RuntimeException(); + } + })); + + safe.onError(new TestException()); + } +} diff --git a/src/test/java/rx/observers/SerializedObserverTest.java b/src/test/java/rx/observers/SerializedObserverTest.java index b469c131d4..a14f146e75 100644 --- a/src/test/java/rx/observers/SerializedObserverTest.java +++ b/src/test/java/rx/observers/SerializedObserverTest.java @@ -15,35 +15,20 @@ */ package rx.observers; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.*; + +import rx.*; import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Subscriber; -import rx.Subscription; +import rx.exceptions.TestException; import rx.schedulers.Schedulers; public class SerializedObserverTest { @@ -813,4 +798,164 @@ protected void captureMaxThreads() { } } + + @Test + public void testSerializeNull() { + final AtomicReference> serial = new AtomicReference>(); + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + if (t != null && t == 0) { + serial.get().onNext(null); + } + super.onNext(t); + } + }; + + SerializedObserver sobs = new SerializedObserver(to); + serial.set(sobs); + + sobs.onNext(0); + + to.assertReceivedOnNext(Arrays.asList(0, null)); + } + + @Test + public void testSerializeAllowsOnError() { + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + SerializedObserver sobs = new SerializedObserver(to); + + try { + sobs.onNext(0); + } catch (TestException ex) { + sobs.onError(ex); + } + + assertEquals(1, to.getOnErrorEvents().size()); + assertTrue(to.getOnErrorEvents().get(0) instanceof TestException); + } + + @Test + public void testSerializeReentrantNullAndComplete() { + final AtomicReference> serial = new AtomicReference>(); + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + serial.get().onCompleted(); + throw new TestException(); + } + }; + + SerializedObserver sobs = new SerializedObserver(to); + serial.set(sobs); + + try { + sobs.onNext(0); + } catch (TestException ex) { + sobs.onError(ex); + } + + assertEquals(1, to.getOnErrorEvents().size()); + assertTrue(to.getOnErrorEvents().get(0) instanceof TestException); + assertTrue(to.getOnCompletedEvents().isEmpty()); + } + + @Test + public void testSerializeReentrantNullAndError() { + final AtomicReference> serial = new AtomicReference>(); + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + serial.get().onError(new RuntimeException()); + throw new TestException(); + } + }; + + SerializedObserver sobs = new SerializedObserver(to); + serial.set(sobs); + + try { + sobs.onNext(0); + } catch (TestException ex) { + sobs.onError(ex); + } + + assertEquals(1, to.getOnErrorEvents().size()); + assertTrue(to.getOnErrorEvents().get(0) instanceof TestException); + assertTrue(to.getOnCompletedEvents().isEmpty()); + } + + @Test + public void testSerializeDrainPhaseThrows() { + final AtomicReference> serial = new AtomicReference>(); + TestObserver to = new TestObserver() { + @Override + public void onNext(Integer t) { + if (t != null && t == 0) { + serial.get().onNext(null); + } else + if (t == null) { + throw new TestException(); + } + super.onNext(t); + } + }; + + SerializedObserver sobs = new SerializedObserver(to); + serial.set(sobs); + + sobs.onNext(0); + + to.assertReceivedOnNext(Arrays.asList(0)); + assertEquals(1, to.getOnErrorEvents().size()); + assertTrue(to.getOnErrorEvents().get(0) instanceof TestException); + } + + @Test + public void testErrorReentry() { + final AtomicReference> serial = new AtomicReference>(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer v) { + serial.get().onError(new TestException()); + serial.get().onError(new TestException()); + super.onNext(v); + } + }; + SerializedObserver sobs = new SerializedObserver(ts); + serial.set(sobs); + + sobs.onNext(1); + + ts.assertValue(1); + ts.assertError(TestException.class); + } + @Test + public void testCompleteReentry() { + final AtomicReference> serial = new AtomicReference>(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer v) { + serial.get().onCompleted(); + serial.get().onCompleted(); + super.onNext(v); + } + }; + SerializedObserver sobs = new SerializedObserver(ts); + serial.set(sobs); + + sobs.onNext(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } } diff --git a/src/test/java/rx/observers/SubscribersTest.java b/src/test/java/rx/observers/SubscribersTest.java new file mode 100644 index 0000000000..241ecae9af --- /dev/null +++ b/src/test/java/rx/observers/SubscribersTest.java @@ -0,0 +1,188 @@ +/** + * 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.observers; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import rx.exceptions.*; +import rx.functions.*; + +public class SubscribersTest { + @Test + public void testNotInstantiable() { + try { + Constructor c = Subscribers.class.getDeclaredConstructor(); + c.setAccessible(true); + Object instance = c.newInstance(); + fail("Could instantiate Actions! " + instance); + } catch (NoSuchMethodException ex) { + ex.printStackTrace(); + } catch (InvocationTargetException ex) { + ex.printStackTrace(); + } catch (InstantiationException ex) { + ex.printStackTrace(); + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + } + } + + @Test + public void testEmptyOnErrorNotImplemented() { + try { + Subscribers.empty().onError(new TestException()); + fail("OnErrorNotImplementedException not thrown!"); + } catch (OnErrorNotImplementedException ex) { + if (!(ex.getCause() instanceof TestException)) { + fail("TestException not wrapped, instead: " + ex.getCause()); + } + } + } + @Test + public void testCreate1OnErrorNotImplemented() { + try { + Subscribers.create(Actions.empty()).onError(new TestException()); + fail("OnErrorNotImplementedException not thrown!"); + } catch (OnErrorNotImplementedException ex) { + if (!(ex.getCause() instanceof TestException)) { + fail("TestException not wrapped, instead: " + ex.getCause()); + } + } + } + @Test(expected = IllegalArgumentException.class) + public void testCreate1Null() { + Subscribers.create(null); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate2Null() { + Action1 throwAction = Actions.empty(); + Subscribers.create(null, throwAction); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate3Null() { + Subscribers.create(Actions.empty(), null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreate4Null() { + Action1 throwAction = Actions.empty(); + Subscribers.create(null, throwAction, Actions.empty()); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate5Null() { + Subscribers.create(Actions.empty(), null, Actions.empty()); + } + @Test(expected = IllegalArgumentException.class) + public void testCreate6Null() { + Action1 throwAction = Actions.empty(); + Subscribers.create(Actions.empty(), throwAction, null); + } + + @Test + public void testCreate1Value() { + final AtomicInteger value = new AtomicInteger(); + Action1 action = new Action1() { + @Override + public void call(Integer t) { + value.set(t); + } + }; + Subscribers.create(action).onNext(1); + + assertEquals(1, value.get()); + } + @Test + public void testCreate2Value() { + final AtomicInteger value = new AtomicInteger(); + Action1 action = new Action1() { + @Override + public void call(Integer t) { + value.set(t); + } + }; + Action1 throwAction = Actions.empty(); + Subscribers.create(action, throwAction).onNext(1); + + assertEquals(1, value.get()); + } + + @Test + public void testCreate3Value() { + final AtomicInteger value = new AtomicInteger(); + Action1 action = new Action1() { + @Override + public void call(Integer t) { + value.set(t); + } + }; + Action1 throwAction = Actions.empty(); + Subscribers.create(action, throwAction, Actions.empty()).onNext(1); + + assertEquals(1, value.get()); + } + + @Test + public void testError2() { + final AtomicReference value = new AtomicReference(); + Action1 action = new Action1() { + @Override + public void call(Throwable t) { + value.set(t); + } + }; + TestException exception = new TestException(); + Subscribers.create(Actions.empty(), action).onError(exception); + + assertEquals(exception, value.get()); + } + + @Test + public void testError3() { + final AtomicReference value = new AtomicReference(); + Action1 action = new Action1() { + @Override + public void call(Throwable t) { + value.set(t); + } + }; + TestException exception = new TestException(); + Subscribers.create(Actions.empty(), action, Actions.empty()).onError(exception); + + assertEquals(exception, value.get()); + } + + @Test + public void testCompleted() { + Action0 action = mock(Action0.class); + + Action1 throwAction = Actions.empty(); + Subscribers.create(Actions.empty(), throwAction, action).onCompleted(); + + verify(action).call(); + } + @Test + public void testEmptyCompleted() { + Subscribers.create(Actions.empty()).onCompleted(); + + Action1 throwAction = Actions.empty(); + Subscribers.create(Actions.empty(), throwAction).onCompleted(); + } +} diff --git a/src/test/java/rx/observers/TestObserverTest.java b/src/test/java/rx/observers/TestObserverTest.java index aa253f2cd2..53f7a06746 100644 --- a/src/test/java/rx/observers/TestObserverTest.java +++ b/src/test/java/rx/observers/TestObserverTest.java @@ -15,20 +15,19 @@ */ package rx.observers; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; -import java.util.Arrays; +import java.util.*; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.ExpectedException; import org.mockito.InOrder; +import rx.Notification; import rx.Observable; import rx.Observer; +import rx.exceptions.TestException; import rx.subjects.PublishSubject; public class TestObserverTest { @@ -124,5 +123,109 @@ public void testWrappingMockWhenUnsubscribeInvolved() { public void testErrorSwallowed() { Observable.error(new RuntimeException()).subscribe(new TestObserver()); } + + @Test + public void testGetEvents() { + TestObserver to = new TestObserver(); + to.onNext(1); + to.onNext(2); + + assertEquals(Arrays.asList(Arrays.asList(1, 2), + Collections.emptyList(), + Collections.emptyList()), to.getEvents()); + + to.onCompleted(); + + assertEquals(Arrays.asList(Arrays.asList(1, 2), Collections.emptyList(), + Collections.singletonList(Notification.createOnCompleted())), to.getEvents()); + + TestException ex = new TestException(); + TestObserver to2 = new TestObserver(); + to2.onNext(1); + to2.onNext(2); + + assertEquals(Arrays.asList(Arrays.asList(1, 2), + Collections.emptyList(), + Collections.emptyList()), to2.getEvents()); + + to2.onError(ex); + + assertEquals(Arrays.asList( + Arrays.asList(1, 2), + Collections.singletonList(ex), + Collections.emptyList()), + to2.getEvents()); + } + @Test + public void testNullExpected() { + TestObserver to = new TestObserver(); + to.onNext(1); + + try { + to.assertReceivedOnNext(Arrays.asList((Integer)null)); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Null element check assertion didn't happen!"); + } + + @Test + public void testNullActual() { + TestObserver to = new TestObserver(); + to.onNext(null); + + try { + to.assertReceivedOnNext(Arrays.asList(1)); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Null element check assertion didn't happen!"); + } + + @Test + public void testTerminalErrorOnce() { + TestObserver to = new TestObserver(); + to.onError(new TestException()); + to.onError(new TestException()); + + try { + to.assertTerminalEvent(); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Failed to report multiple onError terminal events!"); + } + @Test + public void testTerminalCompletedOnce() { + TestObserver to = new TestObserver(); + to.onCompleted(); + to.onCompleted(); + + try { + to.assertTerminalEvent(); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Failed to report multiple onError terminal events!"); + } + + @Test + public void testTerminalOneKind() { + TestObserver to = new TestObserver(); + to.onError(new TestException()); + to.onCompleted(); + + try { + to.assertTerminalEvent(); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Failed to report multiple kinds of events!"); + } } diff --git a/src/test/java/rx/observers/TestSubscriberTest.java b/src/test/java/rx/observers/TestSubscriberTest.java index 75d59fc1f8..1076d2152f 100644 --- a/src/test/java/rx/observers/TestSubscriberTest.java +++ b/src/test/java/rx/observers/TestSubscriberTest.java @@ -15,25 +15,22 @@ */ package rx.observers; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.junit.rules.ExpectedException; import org.mockito.InOrder; -import rx.Observable; -import rx.Observer; +import rx.*; +import rx.Scheduler.Worker; +import rx.exceptions.*; import rx.functions.Action0; +import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; public class TestSubscriberTest { @@ -160,4 +157,442 @@ public void call() { assertTrue(unsub.get()); } + @Test(expected = NullPointerException.class) + public void testNullDelegate1() { + TestSubscriber ts = new TestSubscriber((Observer)null); + ts.onCompleted(); + } + + @Test(expected = NullPointerException.class) + public void testNullDelegate2() { + TestSubscriber ts = new TestSubscriber((Subscriber)null); + ts.onCompleted(); + } + + @Test(expected = NullPointerException.class) + public void testNullDelegate3() { + TestSubscriber ts = new TestSubscriber((Subscriber)null, 0); + ts.onCompleted(); + } + + @Test + public void testDelegate1() { + TestObserver to = new TestObserver(); + TestSubscriber ts = TestSubscriber.create(to); + ts.onCompleted(); + + to.assertTerminalEvent(); + } + + @Test + public void testDelegate2() { + TestSubscriber ts1 = TestSubscriber.create(); + TestSubscriber ts2 = TestSubscriber.create(ts1); + ts2.onCompleted(); + + ts1.assertCompleted(); + } + + @Test + public void testDelegate3() { + TestSubscriber ts1 = TestSubscriber.create(); + TestSubscriber ts2 = TestSubscriber.create(ts1, 0); + ts2.onCompleted(); + ts1.assertCompleted(); + } + + @Test + public void testUnsubscribed() { + TestSubscriber ts = new TestSubscriber(); + try { + ts.assertUnsubscribed(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Not unsubscribed but not reported!"); + } + + @Test + public void testNoErrors() { + TestSubscriber ts = new TestSubscriber(); + ts.onError(new TestException()); + try { + ts.assertNoErrors(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Error present but no assertion error!"); + } + + @Test + public void testNotCompleted() { + TestSubscriber ts = new TestSubscriber(); + try { + ts.assertCompleted(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Not completed and no assertion error!"); + } + + @Test + public void testMultipleCompletions() { + TestSubscriber ts = new TestSubscriber(); + ts.onCompleted(); + ts.onCompleted(); + try { + ts.assertCompleted(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Multiple completions and no assertion error!"); + } + + @Test + public void testCompleted() { + TestSubscriber ts = new TestSubscriber(); + ts.onCompleted(); + try { + ts.assertNotCompleted(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Completed and no assertion error!"); + } + + @Test + public void testMultipleCompletions2() { + TestSubscriber ts = new TestSubscriber(); + ts.onCompleted(); + ts.onCompleted(); + try { + ts.assertNotCompleted(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Multiple completions and no assertion error!"); + } + + @Test + public void testMultipleErrors() { + TestSubscriber ts = new TestSubscriber(); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertNoErrors(); + } catch (AssertionError ex) { + if (!(ex.getCause() instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void testMultipleErrors2() { + TestSubscriber ts = new TestSubscriber(); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(TestException.class); + } catch (AssertionError ex) { + if (!(ex.getCause() instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void testMultipleErrors3() { + TestSubscriber ts = new TestSubscriber(); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + if (!(ex.getCause() instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void testDifferentError() { + TestSubscriber ts = new TestSubscriber(); + ts.onError(new TestException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void testDifferentError2() { + TestSubscriber ts = new TestSubscriber(); + ts.onError(new RuntimeException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void testDifferentError3() { + TestSubscriber ts = new TestSubscriber(); + ts.onError(new RuntimeException()); + try { + ts.assertError(TestException.class); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void testNoError() { + TestSubscriber ts = new TestSubscriber(); + try { + ts.assertError(TestException.class); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void testNoError2() { + TestSubscriber ts = new TestSubscriber(); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void testInterruptTerminalEventAwait() { + TestSubscriber ts = TestSubscriber.create(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Action0() { + @Override + public void call() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + ts.awaitTerminalEvent(); + fail("Did not interrupt wait!"); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof InterruptedException)) { + fail("The cause is not InterruptedException! " + ex.getCause()); + } + } + } finally { + w.unsubscribe(); + } + } + + @Test + public void testInterruptTerminalEventAwaitTimed() { + TestSubscriber ts = TestSubscriber.create(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Action0() { + @Override + public void call() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + ts.awaitTerminalEvent(5, TimeUnit.SECONDS); + fail("Did not interrupt wait!"); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof InterruptedException)) { + fail("The cause is not InterruptedException! " + ex.getCause()); + } + } + } finally { + w.unsubscribe(); + } + } + + @Test + public void testInterruptTerminalEventAwaitAndUnsubscribe() { + TestSubscriber ts = TestSubscriber.create(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Action0() { + @Override + public void call() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + ts.awaitTerminalEventAndUnsubscribeOnTimeout(5, TimeUnit.SECONDS); + if (!ts.isUnsubscribed()) { + fail("Did not unsubscribe!"); + } + } finally { + w.unsubscribe(); + } + } + + @Test + public void testNoTerminalEventBut1Completed() { + TestSubscriber ts = TestSubscriber.create(); + + ts.onCompleted(); + + try { + ts.assertNoTerminalEvent(); + fail("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void testNoTerminalEventBut1Error() { + TestSubscriber ts = TestSubscriber.create(); + + ts.onError(new TestException()); + + try { + ts.assertNoTerminalEvent(); + fail("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void testNoTerminalEventBut1Error1Completed() { + TestSubscriber ts = TestSubscriber.create(); + + ts.onCompleted(); + ts.onError(new TestException()); + + try { + ts.assertNoTerminalEvent(); + fail("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void testNoTerminalEventBut2Errors() { + TestSubscriber ts = TestSubscriber.create(); + + ts.onError(new TestException()); + ts.onError(new TestException()); + + try { + ts.assertNoTerminalEvent(); + fail("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + if (!(ex.getCause() instanceof CompositeException)) { + fail("Did not report a composite exception cause: " + ex.getCause()); + } + } + } + + @Test + public void testNoValues() { + TestSubscriber ts = TestSubscriber.create(); + ts.onNext(1); + + try { + ts.assertNoValues(); + fail("Failed to report there were values!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void testValueCount() { + TestSubscriber ts = TestSubscriber.create(); + ts.onNext(1); + ts.onNext(2); + + try { + ts.assertValueCount(3); + fail("Failed to report there were values!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test(timeout = 1000) + public void testOnCompletedCrashCountsDownLatch() { + TestObserver to = new TestObserver() { + @Override + public void onCompleted() { + throw new TestException(); + } + }; + TestSubscriber ts = TestSubscriber.create(to); + + try { + ts.onCompleted(); + } catch (TestException ex) { + // expected + } + + ts.awaitTerminalEvent(); + } + + @Test(timeout = 1000) + public void testOnErrorCrashCountsDownLatch() { + TestObserver to = new TestObserver() { + @Override + public void onError(Throwable e) { + throw new TestException(); + } + }; + TestSubscriber ts = TestSubscriber.create(to); + + try { + ts.onError(new RuntimeException()); + } catch (TestException ex) { + // expected + } + + ts.awaitTerminalEvent(); + } } From 8ad226067434cd39ce493b336bd0659778625959 Mon Sep 17 00:00:00 2001 From: Yuya Tanaka Date: Thu, 30 Jul 2015 00:26:44 +0900 Subject: [PATCH 352/857] No InterruptedException with synchronous BlockingObservable --- .../operators/BlockingOperatorToIterator.java | 4 + .../rx/observables/BlockingObservable.java | 40 +-- .../operators/BlockingOperatorNextTest.java | 24 +- .../observables/BlockingObservableTest.java | 251 ++++++++++++------ 4 files changed, 215 insertions(+), 104 deletions(-) diff --git a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java index da63d9e227..6f631a211d 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java @@ -93,6 +93,10 @@ public T next() { private Notification take() { try { + Notification poll = notifications.poll(); + if (poll != null) { + return poll; + } return notifications.take(); } catch (InterruptedException e) { subscription.unsubscribe(); diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 8a4ce728cf..4f4fdda412 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -123,17 +123,7 @@ public void onNext(T args) { onNext.call(args); } }); - // block until the subscription completes and then return - try { - latch.await(); - } catch (InterruptedException e) { - subscription.unsubscribe(); - // set the interrupted flag again so callers can still get it - // for more information see https://github.com/ReactiveX/RxJava/pull/147#issuecomment-13624780 - Thread.currentThread().interrupt(); - // using Runtime so it is not checked - throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); - } + awaitForComplete(latch, subscription); if (exceptionFromOnError.get() != null) { if (exceptionFromOnError.get() instanceof RuntimeException) { @@ -456,14 +446,7 @@ public void onNext(final T item) { returnItem.set(item); } }); - - try { - latch.await(); - } catch (InterruptedException e) { - subscription.unsubscribe(); - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); - } + awaitForComplete(latch, subscription); if (returnException.get() != null) { if (returnException.get() instanceof RuntimeException) { @@ -475,4 +458,23 @@ public void onNext(final T item) { return returnItem.get(); } + + private void awaitForComplete(CountDownLatch latch, Subscription subscription) { + if (latch.getCount() == 0) { + // Synchronous observable completes before awaiting for it. + // Skip await so InterruptedException will never be thrown. + return; + } + // block until the subscription completes and then return + try { + latch.await(); + } catch (InterruptedException e) { + subscription.unsubscribe(); + // set the interrupted flag again so callers can still get it + // for more information see https://github.com/ReactiveX/RxJava/pull/147#issuecomment-13624780 + Thread.currentThread().interrupt(); + // using Runtime so it is not checked + throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); + } + } } diff --git a/src/test/java/rx/internal/operators/BlockingOperatorNextTest.java b/src/test/java/rx/internal/operators/BlockingOperatorNextTest.java index e53915d6ae..743a8b3b09 100644 --- a/src/test/java/rx/internal/operators/BlockingOperatorNextTest.java +++ b/src/test/java/rx/internal/operators/BlockingOperatorNextTest.java @@ -15,11 +15,8 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static rx.internal.operators.BlockingOperatorNext.next; +import org.junit.Assert; +import org.junit.Test; import java.util.Iterator; import java.util.NoSuchElementException; @@ -28,19 +25,21 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.Assert; -import org.junit.Test; - import rx.Observable; import rx.Subscriber; import rx.exceptions.TestException; -import rx.internal.operators.BlockingOperatorNext; import rx.observables.BlockingObservable; import rx.schedulers.Schedulers; import rx.subjects.BehaviorSubject; import rx.subjects.PublishSubject; import rx.subjects.Subject; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static rx.internal.operators.BlockingOperatorNext.next; + public class BlockingOperatorNextTest { private void fireOnNextInNewThread(final Subject o, final String value) { @@ -83,6 +82,13 @@ public void testNext() { assertTrue(it.hasNext()); assertEquals("two", it.next()); + fireOnNextInNewThread(obs, "three"); + try { + assertEquals("three", it.next()); + } catch (NoSuchElementException e) { + fail("Calling next() without hasNext() should wait for next fire"); + } + obs.onCompleted(); assertFalse(it.hasNext()); try { diff --git a/src/test/java/rx/observables/BlockingObservableTest.java b/src/test/java/rx/observables/BlockingObservableTest.java index 95b0e21ac5..4328461d80 100644 --- a/src/test/java/rx/observables/BlockingObservableTest.java +++ b/src/test/java/rx/observables/BlockingObservableTest.java @@ -15,21 +15,18 @@ */ package rx.observables; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; @@ -40,6 +37,12 @@ import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + public class BlockingObservableTest { @Mock @@ -357,7 +360,7 @@ public void testFirstOrDefault() { @Test public void testFirstOrDefaultWithEmpty() { - BlockingObservable observable = BlockingObservable.from(Observable. empty()); + BlockingObservable observable = BlockingObservable.from(Observable.empty()); assertEquals("default", observable.firstOrDefault("default")); } @@ -411,117 +414,196 @@ public void call() { assertTrue("Timeout means `unsubscribe` is not called", unsubscribe.await(30, TimeUnit.SECONDS)); } + private Action1> singleAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.single(); + } + }; + @Test public void testUnsubscribeFromSingleWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("single()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.single(); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("single()", singleAction); } + @Test + public void testNoInterruptedExceptionWhenInterruptedWhileSingleOnSynchronousObservable() throws InterruptedException { + new InterruptionTests().assertNoInterruptedExceptionWhenSynchronous("single()", singleAction); + } + + private Action1> forEachAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.forEach(new Action1() { + @Override + public void call(final Void aVoid) { + // nothing + } + }); + } + }; + @Test public void testUnsubscribeFromForEachWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("forEach()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.forEach(new Action1() { - @Override - public void call(final Void aVoid) { - // nothing - } - }); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("forEach()", forEachAction); } + @Test + public void testNoInterruptedExceptionWhenInterruptedWhileForEachOnSynchronousObservable() throws InterruptedException { + new InterruptionTests().assertNoInterruptedExceptionWhenSynchronous("forEach()", forEachAction); + } + + private Action1> firstAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.first(); + } + }; + @Test public void testUnsubscribeFromFirstWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("first()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.first(); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("first()", firstAction); } + @Test + public void testNoInterruptedExceptionWhenInterruptedWhileFirstOnSynchronousObservable() throws InterruptedException { + new InterruptionTests().assertNoInterruptedExceptionWhenSynchronous("first()", firstAction); + } + + private Action1> lastAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.last(); + } + }; + @Test public void testUnsubscribeFromLastWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("last()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.last(); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("last()", lastAction); + } + + @Test + public void testNoInterruptedExceptionWhenInterruptedWhileLastOnSynchronousObservable() throws InterruptedException { + new InterruptionTests().assertNoInterruptedExceptionWhenSynchronous("last()", lastAction); } + private Action1> latestAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.latest().iterator().next(); + } + }; + @Test public void testUnsubscribeFromLatestWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("latest()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.latest().iterator().next(); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("latest()", latestAction); } + // NOTE: latest() is intended to be async, so InterruptedException will be thrown even if synchronous + + private Action1> nextAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.next().iterator().next(); + } + }; + @Test public void testUnsubscribeFromNextWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("next()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.next().iterator().next(); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("next()", nextAction); } + // NOTE: next() is intended to be async, so InterruptedException will be thrown even if synchronous + + private Action1> getIteratorAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.getIterator().next(); + } + }; + @Test public void testUnsubscribeFromGetIteratorWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("getIterator()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.getIterator().next(); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("getIterator()", getIteratorAction); } + @Test + public void testNoInterruptedExceptionWhenInterruptedWhileGetIteratorOnSynchronousObservable() throws InterruptedException { + new InterruptionTests().assertNoInterruptedExceptionWhenSynchronous("getIterator()", getIteratorAction); + } + + private Action1> toIterableAction = new Action1>() { + @Override + public void call(final BlockingObservable o) { + o.toIterable().iterator().next(); + } + }; + @Test public void testUnsubscribeFromToIterableWhenInterrupted() throws InterruptedException { - new InterruptionTests().assertUnsubscribeIsInvoked("toIterable()", new Action1>() { - @Override - public void call(final BlockingObservable o) { - o.toIterable().iterator().next(); - } - }); + new InterruptionTests().assertUnsubscribeIsInvoked("toIterable()", toIterableAction); + } + + @Test + public void testNoInterruptedExceptionWhenInterruptedWhileToIterableOnSynchronousObservable() throws InterruptedException { + new InterruptionTests().assertNoInterruptedExceptionWhenSynchronous("toIterable()", toIterableAction); } /** Utilities set for interruption behaviour tests. */ private static class InterruptionTests { private boolean isUnSubscribed; - private RuntimeException error; + private final AtomicReference errorRef = new AtomicReference(); private CountDownLatch latch = new CountDownLatch(1); - private Observable createObservable() { - return Observable.never().doOnUnsubscribe(new Action0() { + private Action0 createOnUnsubscribe() { + return new Action0() { @Override public void call() { isUnSubscribed = true; } - }); + }; + } + + private Observable createNeverObservable() { + return Observable.never().doOnUnsubscribe(createOnUnsubscribe()); } - private void startBlockingAndInterrupt(final Action1> blockingAction) { + private Observable createSynchronousObservable() { + return Observable.from(new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private boolean nextCalled = false; + + @Override + public boolean hasNext() { + return !(nextCalled && Thread.currentThread().isInterrupted()); + } + + @Override + public Void next() { + nextCalled = true; + return null; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read-only iterator."); + } + }; + } + }).takeLast(1).doOnUnsubscribe(createOnUnsubscribe()); + } + + private void startBlockingAndInterrupt(final Observable observable, final Action1> blockingAction) { Thread subscriptionThread = new Thread() { @Override public void run() { try { - blockingAction.call(createObservable().toBlocking()); + blockingAction.call(observable.toBlocking()); } catch (RuntimeException e) { - if (!(e.getCause() instanceof InterruptedException)) { - error = e; - } + errorRef.set(e); } latch.countDown(); } @@ -532,14 +614,31 @@ public void run() { void assertUnsubscribeIsInvoked(final String method, final Action1> blockingAction) throws InterruptedException { - startBlockingAndInterrupt(blockingAction); + startBlockingAndInterrupt(createNeverObservable(), blockingAction); assertTrue("Timeout means interruption is not performed", latch.await(30, TimeUnit.SECONDS)); - if (error != null) { - throw error; - } + assertNotNull("InterruptedException is not thrown", getInterruptedExceptionOrNull()); assertTrue("'unsubscribe' is not invoked when thread is interrupted for " + method, isUnSubscribed); } + void assertNoInterruptedExceptionWhenSynchronous(final String method, final Action1> blockingAction) + throws InterruptedException { + startBlockingAndInterrupt(createSynchronousObservable(), blockingAction); + assertTrue("Timeout means interruption is not performed", latch.await(30, TimeUnit.SECONDS)); + assertNull("'InterruptedException' is thrown when observable is synchronous for " + method, getInterruptedExceptionOrNull()); + } + + private InterruptedException getInterruptedExceptionOrNull() { + RuntimeException error = errorRef.get(); + if (error == null) { + return null; + } + Throwable cause = error.getCause(); + if (cause instanceof InterruptedException) { + return (InterruptedException) cause; + } + throw error; + } + } } From 5cb9f418250009498b536de46e87c247ca6b5e68 Mon Sep 17 00:00:00 2001 From: Jacek Rzeniewicz Date: Thu, 30 Jul 2015 00:29:16 +0100 Subject: [PATCH 353/857] Remove redundant type parameter in EmptyAction --- src/main/java/rx/functions/Actions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/functions/Actions.java b/src/main/java/rx/functions/Actions.java index 2002995487..342cfd030c 100644 --- a/src/main/java/rx/functions/Actions.java +++ b/src/main/java/rx/functions/Actions.java @@ -24,14 +24,14 @@ private Actions() { } @SuppressWarnings("unchecked") - public static final EmptyAction empty() { + public static final EmptyAction empty() { return EMPTY_ACTION; } @SuppressWarnings("rawtypes") private static final EmptyAction EMPTY_ACTION = new EmptyAction(); - private static final class EmptyAction implements + private static final class EmptyAction implements Action0, Action1, Action2, From dd5c6464f04fe07ab8562e85486e0b563cb543ab Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Wed, 29 Jul 2015 21:13:09 +0300 Subject: [PATCH 354/857] Improve performance of NewThreadWorker.tryEnableCancelPolicy(). Disable search for ScheduledExecutorService.setRemoveOnCancelPolicy() on Android API < 21 --- .../internal/schedulers/NewThreadWorker.java | 104 +++++++++++++++--- .../rx/internal/util/PlatformDependent.java | 46 ++++++-- .../schedulers/NewThreadWorkerTest.java | 65 +++++++++++ 3 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 src/test/java/rx/internal/schedulers/NewThreadWorkerTest.java diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index 094c94892f..4c47936871 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -27,6 +27,8 @@ import rx.plugins.*; import rx.subscriptions.*; +import static rx.internal.util.PlatformDependent.ANDROID_API_VERSION_IS_NOT_ANDROID; + /** * @warn class description missing */ @@ -39,8 +41,7 @@ public class NewThreadWorker extends Scheduler.Worker implements Subscription { /** Force the use of purge (true/false). */ private static final String PURGE_FORCE_KEY = "rx.scheduler.jdk6.purge-force"; private static final String PURGE_THREAD_PREFIX = "RxSchedulerPurge-"; - /** Forces the use of purge even if setRemoveOnCancelPolicy is available. */ - private static final boolean PURGE_FORCE; + private static final boolean SHOULD_TRY_ENABLE_CANCEL_POLICY; /** The purge frequency in milliseconds. */ public static final int PURGE_FREQUENCY; private static final ConcurrentHashMap EXECUTORS; @@ -48,8 +49,17 @@ public class NewThreadWorker extends Scheduler.Worker implements Subscription { static { EXECUTORS = new ConcurrentHashMap(); PURGE = new AtomicReference(); - PURGE_FORCE = Boolean.getBoolean(PURGE_FORCE_KEY); PURGE_FREQUENCY = Integer.getInteger(FREQUENCY_KEY, 1000); + + // Forces the use of purge even if setRemoveOnCancelPolicy is available + final boolean purgeForce = Boolean.getBoolean(PURGE_FORCE_KEY); + + final int androidApiVersion = PlatformDependent.getAndroidApiVersion(); + + // According to http://developer.android.com/reference/java/util/concurrent/ScheduledThreadPoolExecutor.html#setRemoveOnCancelPolicy(boolean) + // setRemoveOnCancelPolicy available since Android API 21 + SHOULD_TRY_ENABLE_CANCEL_POLICY = !purgeForce + && (androidApiVersion == ANDROID_API_VERSION_IS_NOT_ANDROID || androidApiVersion >= 21); } /** * Registers the given executor service and starts the purge thread if not already started. @@ -85,6 +95,7 @@ public void run() { public static void deregisterExecutor(ScheduledExecutorService service) { EXECUTORS.remove(service); } + /** Purges each registered executor and eagerly evicts shutdown executors. */ static void purgeExecutors() { try { @@ -102,32 +113,89 @@ static void purgeExecutors() { RxJavaPlugins.getInstance().getErrorHandler().handleError(t); } } - - /** + + /** + * Improves performance of {@link #tryEnableCancelPolicy(ScheduledExecutorService)}. + * Also, it works even for inheritance: {@link Method} of base class can be invoked on the instance of child class. + */ + private static volatile Object cachedSetRemoveOnCancelPolicyMethod; + + /** + * Possible value of {@link #cachedSetRemoveOnCancelPolicyMethod} which means that cancel policy is not supported. + */ + private static final Object SET_REMOVE_ON_CANCEL_POLICY_METHOD_NOT_SUPPORTED = new Object(); + + /** * Tries to enable the Java 7+ setRemoveOnCancelPolicy. *

{@code public} visibility reason: called from other package(s) within RxJava. * If the method returns false, the {@link #registerExecutor(ScheduledThreadPoolExecutor)} may * be called to enable the backup option of purging the executors. - * @param exec the executor to call setRemoveOnCaneclPolicy if available. + * @param executor the executor to call setRemoveOnCaneclPolicy if available. * @return true if the policy was successfully enabled */ - public static boolean tryEnableCancelPolicy(ScheduledExecutorService exec) { - if (!PURGE_FORCE) { - for (Method m : exec.getClass().getMethods()) { - if (m.getName().equals("setRemoveOnCancelPolicy") - && m.getParameterTypes().length == 1 - && m.getParameterTypes()[0] == Boolean.TYPE) { - try { - m.invoke(exec, true); - return true; - } catch (Exception ex) { - RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); - } + public static boolean tryEnableCancelPolicy(ScheduledExecutorService executor) { + if (SHOULD_TRY_ENABLE_CANCEL_POLICY) { + final boolean isInstanceOfScheduledThreadPoolExecutor = executor instanceof ScheduledThreadPoolExecutor; + + final Method methodToCall; + + if (isInstanceOfScheduledThreadPoolExecutor) { + final Object localSetRemoveOnCancelPolicyMethod = cachedSetRemoveOnCancelPolicyMethod; + + if (localSetRemoveOnCancelPolicyMethod == SET_REMOVE_ON_CANCEL_POLICY_METHOD_NOT_SUPPORTED) { + return false; + } + + if (localSetRemoveOnCancelPolicyMethod == null) { + Method method = findSetRemoveOnCancelPolicyMethod(executor); + + cachedSetRemoveOnCancelPolicyMethod = method != null + ? method + : SET_REMOVE_ON_CANCEL_POLICY_METHOD_NOT_SUPPORTED; + + methodToCall = method; + } else { + methodToCall = (Method) localSetRemoveOnCancelPolicyMethod; + } + } else { + methodToCall = findSetRemoveOnCancelPolicyMethod(executor); + } + + if (methodToCall != null) { + try { + methodToCall.invoke(executor, true); + return true; + } catch (Exception e) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); } } } + return false; } + + /** + * Tries to find {@code "setRemoveOnCancelPolicy(boolean)"} method in the class of passed executor. + * + * @param executor whose class will be used to search for required method. + * @return {@code "setRemoveOnCancelPolicy(boolean)"} {@link Method} + * or {@code null} if required {@link Method} was not found. + */ + static Method findSetRemoveOnCancelPolicyMethod(ScheduledExecutorService executor) { + // The reason for the loop is to avoid NoSuchMethodException being thrown on JDK 6 + // which is more costly than looping through ~70 methods. + for (final Method method : executor.getClass().getMethods()) { + if (method.getName().equals("setRemoveOnCancelPolicy")) { + final Class[] parameterTypes = method.getParameterTypes(); + + if (parameterTypes.length == 1 && parameterTypes[0] == Boolean.TYPE) { + return method; + } + } + } + + return null; + } /* package */ public NewThreadWorker(ThreadFactory threadFactory) { diff --git a/src/main/java/rx/internal/util/PlatformDependent.java b/src/main/java/rx/internal/util/PlatformDependent.java index c6b7f3c28f..614e327a7e 100644 --- a/src/main/java/rx/internal/util/PlatformDependent.java +++ b/src/main/java/rx/internal/util/PlatformDependent.java @@ -20,31 +20,55 @@ /** * Allow platform dependent logic such as checks for Android. - * + * * Modeled after Netty with some code copy/pasted from: https://github.com/netty/netty/blob/master/common/src/main/java/io/netty/util/internal/PlatformDependent.java */ public final class PlatformDependent { - private static final boolean IS_ANDROID = isAndroid0(); + /** + * Possible value of {@link #getAndroidApiVersion()} which means that the current platform is not Android. + */ + public static final int ANDROID_API_VERSION_IS_NOT_ANDROID = 0; + + private static final int ANDROID_API_VERSION = resolveAndroidApiVersion(); + + private static final boolean IS_ANDROID = ANDROID_API_VERSION != ANDROID_API_VERSION_IS_NOT_ANDROID; /** - * Returns {@code true} if and only if the current platform is Android + * Returns {@code true} if and only if the current platform is Android. */ public static boolean isAndroid() { return IS_ANDROID; } - private static boolean isAndroid0() { - boolean android; + /** + * Returns version of Android API. + * + * @return version of Android API or {@link #ANDROID_API_VERSION_IS_NOT_ANDROID } if version + * can not be resolved or if current platform is not Android. + */ + public static int getAndroidApiVersion() { + return ANDROID_API_VERSION; + } + + /** + * Resolves version of Android API. + * + * @return version of Android API or {@link #ANDROID_API_VERSION_IS_NOT_ANDROID} if version can not be resolved + * or if the current platform is not Android. + * @see Documentation + */ + private static int resolveAndroidApiVersion() { try { - Class.forName("android.app.Application", false, getSystemClassLoader()); - android = true; + return (Integer) Class + .forName("android.os.Build$VERSION", true, getSystemClassLoader()) + .getField("SDK_INT") + .get(null); } catch (Exception e) { - // Failed to load the class uniquely available in Android. - android = false; + // Can not resolve version of Android API, maybe current platform is not Android + // or API of resolving current Version of Android API has changed in some release of Android + return ANDROID_API_VERSION_IS_NOT_ANDROID; } - - return android; } /** diff --git a/src/test/java/rx/internal/schedulers/NewThreadWorkerTest.java b/src/test/java/rx/internal/schedulers/NewThreadWorkerTest.java new file mode 100644 index 0000000000..c0d6f93dda --- /dev/null +++ b/src/test/java/rx/internal/schedulers/NewThreadWorkerTest.java @@ -0,0 +1,65 @@ +package rx.internal.schedulers; + +import org.junit.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.lang.reflect.Modifier.FINAL; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class NewThreadWorkerTest { + + @Test + public void findSetRemoveOnCancelPolicyMethodShouldFindMethod() { + ScheduledExecutorService executor = spy(new ScheduledThreadPoolExecutor(1)); + Method setRemoveOnCancelPolicyMethod = NewThreadWorker.findSetRemoveOnCancelPolicyMethod(executor); + + assertNotNull(setRemoveOnCancelPolicyMethod); + assertEquals("setRemoveOnCancelPolicy", setRemoveOnCancelPolicyMethod.getName()); + assertEquals(1, setRemoveOnCancelPolicyMethod.getParameterTypes().length); + assertEquals(Boolean.TYPE, setRemoveOnCancelPolicyMethod.getParameterTypes()[0]); + verifyZeroInteractions(executor); + } + + @Test + public void findSetRemoveOnCancelPolicyMethodShouldNotFindMethod() { + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + + Method setRemoveOnCancelPolicyMethod = NewThreadWorker.findSetRemoveOnCancelPolicyMethod(executor); + assertNull(setRemoveOnCancelPolicyMethod); + verifyZeroInteractions(executor); + } + + private static abstract class ScheduledExecutorServiceWithSetRemoveOnCancelPolicy implements ScheduledExecutorService { + // Just declaration of required method to allow run tests on JDK 6 + public void setRemoveOnCancelPolicy(@SuppressWarnings("UnusedParameters") boolean value) {} + } + + @Test + public void tryEnableCancelPolicyShouldInvokeMethodOnExecutor() { + ScheduledExecutorServiceWithSetRemoveOnCancelPolicy executor + = mock(ScheduledExecutorServiceWithSetRemoveOnCancelPolicy.class); + + boolean result = NewThreadWorker.tryEnableCancelPolicy(executor); + + assertTrue(result); + verify(executor).setRemoveOnCancelPolicy(true); + verifyNoMoreInteractions(executor); + } + + @Test + public void tryEnableCancelPolicyShouldNotInvokeMethodOnExecutor() { + // This executor does not have setRemoveOnCancelPolicy method + ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + + boolean result = NewThreadWorker.tryEnableCancelPolicy(executor); + + assertFalse(result); + verifyZeroInteractions(executor); + } +} From d000c1035b5a4134b12c7ef5198aa8487451e54e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 3 Aug 2015 21:26:22 +0200 Subject: [PATCH 355/857] Fix retry with predicate ignoring backpressure. --- src/main/java/rx/Observable.java | 2 + .../operators/OperatorRetryWithPredicate.java | 116 ++++++++++-------- .../OperatorRetryWithPredicateTest.java | 41 ++++++- 3 files changed, 105 insertions(+), 54 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 0aafecbf79..a6432a89a0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -6576,6 +6576,8 @@ public final Observable retry(final long count) { *

* *

+ *
Backpressure Support:
+ *
This operator honors backpressure. *
Scheduler:
*
{@code retry} operates by default on the {@code trampoline} {@link Scheduler}.
*
diff --git a/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java b/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java index 24beeec2a0..bdfcd3dbeb 100644 --- a/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java @@ -16,11 +16,14 @@ package rx.internal.operators; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + import rx.Observable; +import rx.Producer; import rx.Scheduler; import rx.Subscriber; import rx.functions.Action0; import rx.functions.Func2; +import rx.internal.producers.ProducerArbiter; import rx.schedulers.Schedulers; import rx.subscriptions.SerialSubscription; @@ -38,8 +41,9 @@ public Subscriber> call(final Subscriber child) final SerialSubscription serialSubscription = new SerialSubscription(); // add serialSubscription so it gets unsubscribed if child is unsubscribed child.add(serialSubscription); - - return new SourceSubscriber(child, predicate, inner, serialSubscription); + ProducerArbiter pa = new ProducerArbiter(); + child.setProducer(pa); + return new SourceSubscriber(child, predicate, inner, serialSubscription, pa); } static final class SourceSubscriber extends Subscriber> { @@ -47,79 +51,89 @@ static final class SourceSubscriber extends Subscriber> { final Func2 predicate; final Scheduler.Worker inner; final SerialSubscription serialSubscription; + final ProducerArbiter pa; volatile int attempts; @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ATTEMPTS_UPDATER = AtomicIntegerFieldUpdater.newUpdater(SourceSubscriber.class, "attempts"); - public SourceSubscriber(Subscriber child, final Func2 predicate, Scheduler.Worker inner, - SerialSubscription serialSubscription) { + public SourceSubscriber(Subscriber child, + final Func2 predicate, + Scheduler.Worker inner, + SerialSubscription serialSubscription, + ProducerArbiter pa) { this.child = child; this.predicate = predicate; this.inner = inner; this.serialSubscription = serialSubscription; + this.pa = pa; } @Override - public void onCompleted() { - // ignore as we expect a single nested Observable - } + public void onCompleted() { + // ignore as we expect a single nested Observable + } - @Override - public void onError(Throwable e) { - child.onError(e); - } + @Override + public void onError(Throwable e) { + child.onError(e); + } - @Override - public void onNext(final Observable o) { - inner.schedule(new Action0() { + @Override + public void onNext(final Observable o) { + inner.schedule(new Action0() { - @Override - public void call() { - final Action0 _self = this; - ATTEMPTS_UPDATER.incrementAndGet(SourceSubscriber.this); + @Override + public void call() { + final Action0 _self = this; + ATTEMPTS_UPDATER.incrementAndGet(SourceSubscriber.this); - // new subscription each time so if it unsubscribes itself it does not prevent retries - // by unsubscribing the child subscription - Subscriber subscriber = new Subscriber() { - boolean done; - @Override - public void onCompleted() { - if (!done) { - done = true; - child.onCompleted(); - } + // new subscription each time so if it unsubscribes itself it does not prevent retries + // by unsubscribing the child subscription + Subscriber subscriber = new Subscriber() { + boolean done; + @Override + public void onCompleted() { + if (!done) { + done = true; + child.onCompleted(); } + } - @Override - public void onError(Throwable e) { - if (!done) { - done = true; - if (predicate.call(attempts, e) && !inner.isUnsubscribed()) { - // retry again - inner.schedule(_self); - } else { - // give up and pass the failure - child.onError(e); - } + @Override + public void onError(Throwable e) { + if (!done) { + done = true; + if (predicate.call(attempts, e) && !inner.isUnsubscribed()) { + // retry again + inner.schedule(_self); + } else { + // give up and pass the failure + child.onError(e); } } + } - @Override - public void onNext(T v) { - if (!done) { - child.onNext(v); - } + @Override + public void onNext(T v) { + if (!done) { + child.onNext(v); + pa.produced(1); } + } - }; - // register this Subscription (and unsubscribe previous if exists) - serialSubscription.set(subscriber); - o.unsafeSubscribe(subscriber); - } - }); - } + @Override + public void setProducer(Producer p) { + pa.setProducer(p); + } + }; + // register this Subscription (and unsubscribe previous if exists) + serialSubscription.set(subscriber); + o.unsafeSubscribe(subscriber); + } + }); + } } } diff --git a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java index 76461e3ddf..df878de13a 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryWithPredicateTest.java @@ -20,20 +20,27 @@ import static org.mockito.Mockito.*; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.mockito.InOrder; -import rx.*; +import rx.Observable; import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; import rx.exceptions.TestException; -import rx.functions.*; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.functions.Func2; import rx.observers.TestSubscriber; import rx.subjects.PublishSubject; @@ -360,4 +367,32 @@ public void call(Long t) { }}); assertEquals(Arrays.asList(1L,1L,2L,3L), list); } + @Test + public void testBackpressure() { + final List requests = new ArrayList(); + + Observable source = Observable + .just(1) + .concatWith(Observable.error(new TestException())) + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requests.add(t); + } + }); + + TestSubscriber ts = TestSubscriber.create(3); + source + .retry(new Func2() { + @Override + public Boolean call(Integer t1, Throwable t2) { + return t1 < 3; + } + }).subscribe(ts); + + assertEquals(Arrays.asList(3L, 2L, 1L), requests); + ts.assertValues(1, 1, 1); + ts.assertNotCompleted(); + ts.assertNoErrors(); + } } From bb89744b6b934fa43a187f2d5199c62bcf31e5d3 Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 6 Aug 2015 09:48:29 -0700 Subject: [PATCH 356/857] Add links to page that explains The Observable Contract --- src/main/java/rx/Observable.java | 8 +++++--- src/main/java/rx/internal/util/RxRingBuffer.java | 5 +++-- src/main/java/rx/observables/BlockingObservable.java | 2 +- src/main/java/rx/observers/SafeSubscriber.java | 2 +- src/main/java/rx/subjects/SerializedSubject.java | 4 ++-- src/main/java/rx/subjects/Subject.java | 8 +++++--- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index a6432a89a0..17937609a0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -6830,8 +6830,8 @@ public final Observable scan(R initialValue, Func2 accum } /** - * Forces an Observable's emissions and notifications to be serialized and for it to obey the Rx contract - * in other ways. + * Forces an Observable's emissions and notifications to be serialized and for it to obey + * the Observable contract in other ways. *

* It is possible for an Observable to invoke its Subscribers' methods asynchronously, perhaps from * different threads. This could make such an Observable poorly-behaved, in that it might try to invoke @@ -7672,7 +7672,9 @@ public void onNext(T t) { * error handling, unsubscribe, or execution hooks. *

* Use this only for implementing an {@link Operator} that requires nested subscriptions. For other - * purposes, use {@link #subscribe(Subscriber)} which ensures the Rx contract and other functionality. + * purposes, use {@link #subscribe(Subscriber)} which ensures + * the Observable contract and other + * functionality. *

*
Scheduler:
*
{@code unsafeSubscribe} does not operate by default on a particular {@link Scheduler}.
diff --git a/src/main/java/rx/internal/util/RxRingBuffer.java b/src/main/java/rx/internal/util/RxRingBuffer.java index 7498b445be..f038b2deec 100644 --- a/src/main/java/rx/internal/util/RxRingBuffer.java +++ b/src/main/java/rx/internal/util/RxRingBuffer.java @@ -26,8 +26,9 @@ import rx.internal.util.unsafe.UnsafeAccess; /** - * This assumes Spsc or Spmc usage. This means only a single producer calling the on* methods. This is the Rx contract of an Observer. - * Concurrent invocations of on* methods will not be thread-safe. + * This assumes Spsc or Spmc usage. This means only a single producer calling the on* methods. This is the Rx + * contract of an Observer (see http://reactivex.io/documentation/contract.html). Concurrent invocations of + * on* methods will not be thread-safe. */ public class RxRingBuffer implements Subscription { diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 4f4fdda412..7eced68981 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -96,7 +96,7 @@ public void forEach(final Action1 onNext) { /* * Use 'subscribe' instead of 'unsafeSubscribe' for Rx contract behavior - * as this is the final subscribe in the chain. + * (see http://reactivex.io/documentation/contract.html) as this is the final subscribe in the chain. */ Subscription subscription = o.subscribe(new Subscriber() { @Override diff --git a/src/main/java/rx/observers/SafeSubscriber.java b/src/main/java/rx/observers/SafeSubscriber.java index c81cd4fbff..0181887c34 100644 --- a/src/main/java/rx/observers/SafeSubscriber.java +++ b/src/main/java/rx/observers/SafeSubscriber.java @@ -26,7 +26,7 @@ /** * {@code SafeSubscriber} is a wrapper around {@code Subscriber} that ensures that the {@code Subscriber} - * complies with the Rx contract. + * complies with the Observable contract. *

* The following is taken from the Rx Design Guidelines * document: diff --git a/src/main/java/rx/subjects/SerializedSubject.java b/src/main/java/rx/subjects/SerializedSubject.java index baaf50b8d4..edf4caeefe 100644 --- a/src/main/java/rx/subjects/SerializedSubject.java +++ b/src/main/java/rx/subjects/SerializedSubject.java @@ -24,8 +24,8 @@ *

* When you use an ordinary {@link Subject} as a {@link Subscriber}, you must take care not to call its * {@link Subscriber#onNext} method (or its other {@code on} methods) from multiple threads, as this could lead - * to non-serialized calls, which violates the Observable contract and creates an ambiguity in the resulting - * Subject. + * to non-serialized calls, which violates the + * Observable contract and creates an ambiguity in the resulting Subject. *

* To protect a {@code Subject} from this danger, you can convert it into a {@code SerializedSubject} with code * like the following: diff --git a/src/main/java/rx/subjects/Subject.java b/src/main/java/rx/subjects/Subject.java index 4e5db6b770..075dfe8e93 100644 --- a/src/main/java/rx/subjects/Subject.java +++ b/src/main/java/rx/subjects/Subject.java @@ -40,10 +40,12 @@ protected Subject(OnSubscribe onSubscribe) { *

* When you use an ordinary {@link Subject} as a {@link Subscriber}, you must take care not to call its * {@link Subscriber#onNext} method (or its other {@code on} methods) from multiple threads, as this could - * lead to non-serialized calls, which violates the Observable contract and creates an ambiguity in the resulting Subject. + * lead to non-serialized calls, which violates + * the Observable contract and creates an + * ambiguity in the resulting Subject. *

- * To protect a {@code Subject} from this danger, you can convert it into a {@code SerializedSubject} with code - * like the following: + * To protect a {@code Subject} from this danger, you can convert it into a {@code SerializedSubject} with + * code like the following: *

{@code
      * mySafeSubject = myUnsafeSubject.toSerialized();
      * }
From fc0c0d734fe0daab62d12b74459f79c7f1fb3187 Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Thu, 16 Jul 2015 00:59:11 -0500 Subject: [PATCH 357/857] Implemented Observable.x(ConversionFunc) to allow external extensions to Observables. --- src/main/java/rx/Observable.java | 34 ++- .../java/rx/ObservableConversionTest.java | 234 ++++++++++++++++++ src/test/java/rx/ObservableTests.java | 16 +- 3 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 src/test/java/rx/ObservableConversionTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 17937609a0..246e54f023 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -109,6 +109,23 @@ public interface Operator extends Func1, Subscriber< // cover for generics insanity } + /** + * Passes all emitted values from {@code this} Observable to the provided {@link ConversionFunc} to be + * collected and returned as a single value. Note that it is legal for a {@link ConversionFunc} to + * return an Observable (enabling chaining). + * + * @param conversion a function that converts from this {@code Observable} to an {@code R} + * @return an instance of R created by the provided Conversion + */ + @Experimental + public R x(Func1, ? extends R> conversion) { + return conversion.call(new OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + subscriber.add(Observable.subscribe(subscriber, Observable.this)); + }}); + } + /** * Lifts a function to the current Observable and returns a new Observable that when subscribed to will pass * the values of the current Observable through the Operator function. @@ -127,17 +144,17 @@ public interface Operator extends Func1, Subscriber< *
{@code lift} does not operate by default on a particular {@link Scheduler}.
*
* - * @param lift the Operator that implements the Observable-operating function to be applied to the source + * @param operator the Operator that implements the Observable-operating function to be applied to the source * Observable * @return an Observable that is the result of applying the lifted Operator to the source Observable * @see RxJava wiki: Implementing Your Own Operators */ - public final Observable lift(final Operator lift) { + public final Observable lift(final Operator operator) { return new Observable(new OnSubscribe() { @Override public void call(Subscriber o) { try { - Subscriber st = hook.onLift(lift).call(o); + Subscriber st = hook.onLift(operator).call(o); try { // new Subscriber created and being subscribed with so 'onStart' it st.onStart(); @@ -163,7 +180,6 @@ public void call(Subscriber o) { }); } - /** * Transform an Observable by applying a particular Transformer function to it. *

@@ -7752,11 +7768,15 @@ public final Subscription unsafeSubscribe(Subscriber subscriber) { * @see ReactiveX operators documentation: Subscribe */ public final Subscription subscribe(Subscriber subscriber) { - // validate and proceed + return Observable.subscribe(subscriber, this); + } + + private static Subscription subscribe(Subscriber subscriber, Observable observable) { + // validate and proceed if (subscriber == null) { throw new IllegalArgumentException("observer can not be null"); } - if (onSubscribe == null) { + if (observable.onSubscribe == null) { throw new IllegalStateException("onSubscribe function can not be null."); /* * the subscribe function can also be overridden but generally that's not the appropriate approach @@ -7780,7 +7800,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 - hook.onSubscribeStart(this, onSubscribe).call(subscriber); + hook.onSubscribeStart(observable, observable.onSubscribe).call(subscriber); return hook.onSubscribeReturn(subscriber); } catch (Throwable e) { // special handling for certain Throwable/Error/Exception types diff --git a/src/test/java/rx/ObservableConversionTest.java b/src/test/java/rx/ObservableConversionTest.java new file mode 100644 index 0000000000..543c44780b --- /dev/null +++ b/src/test/java/rx/ObservableConversionTest.java @@ -0,0 +1,234 @@ +package rx; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static junit.framework.Assert.*; + +import org.junit.Test; + +import rx.Observable.OnSubscribe; +import rx.Observable.Operator; +import rx.exceptions.OnErrorNotImplementedException; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.internal.operators.OperatorFilter; +import rx.internal.operators.OperatorMap; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; + +public class ObservableConversionTest { + + public static class Cylon {} + + public static class Jail { + Object cylon; + + Jail(Object cylon) { + this.cylon = cylon; + } + } + + public static class CylonDetectorObservable { + protected OnSubscribe onSubscribe; + + public static CylonDetectorObservable create(OnSubscribe onSubscribe) { + return new CylonDetectorObservable(onSubscribe); + } + + protected CylonDetectorObservable(OnSubscribe onSubscribe) { + this.onSubscribe = onSubscribe; + } + + public void subscribe(Subscriber subscriber) { + onSubscribe.call(subscriber); + } + + public CylonDetectorObservable lift(Operator operator) { + return x(new RobotConversionFunc(operator)); + } + + public O x(Func1, O> operator) { + return operator.call(onSubscribe); + } + + public CylonDetectorObservable compose(Func1, CylonDetectorObservable> transformer) { + return transformer.call(this); + } + + public final CylonDetectorObservable beep(Func1 predicate) { + return lift(new OperatorFilter(predicate)); + } + + public final CylonDetectorObservable boop(Func1 func) { + return lift(new OperatorMap(func)); + } + + public CylonDetectorObservable DESTROY() { + return boop(new Func1() { + @Override + public String call(T t) { + Object cylon = ((Jail) t).cylon; + throwOutTheAirlock(cylon); + if (t instanceof Jail) { + String name = cylon.toString(); + return "Cylon '" + name + "' has been destroyed"; + } + else { + return "Cylon 'anonymous' has been destroyed"; + } + }}); + } + + private static void throwOutTheAirlock(Object cylon) { + // ... + } + } + + public static class RobotConversionFunc implements Func1, CylonDetectorObservable> { + private Operator operator; + + public RobotConversionFunc(Operator operator) { + this.operator = operator; + } + + @Override + public CylonDetectorObservable call(final OnSubscribe onSubscribe) { + return CylonDetectorObservable.create(new OnSubscribe() { + @Override + public void call(Subscriber o) { + try { + Subscriber st = operator.call(o); + try { + st.onStart(); + onSubscribe.call(st); + } catch (OnErrorNotImplementedException e) { + throw e; + } catch (Throwable e) { + st.onError(e); + } + } catch (OnErrorNotImplementedException e) { + throw e; + } catch (Throwable e) { + o.onError(e); + } + + }}); + } + } + + public static class ConvertToCylonDetector implements Func1, CylonDetectorObservable> { + @Override + public CylonDetectorObservable call(final OnSubscribe onSubscribe) { + return CylonDetectorObservable.create(onSubscribe); + } + } + + public static class ConvertToObservable implements Func1, Observable> { + @Override + public Observable call(final OnSubscribe onSubscribe) { + return Observable.create(onSubscribe); + } + } + + @Test + public void testConversionBetweenObservableClasses() { + final TestSubscriber subscriber = new TestSubscriber(new Subscriber(){ + + @Override + public void onCompleted() { + System.out.println("Complete"); + } + + @Override + public void onError(Throwable e) { + System.out.println("error: " + e.getMessage()); + e.printStackTrace(); + } + + @Override + public void onNext(String t) { + System.out.println(t); + }}); + List crewOfBattlestarGalactica = Arrays.asList(new Object[] {"William Adama", "Laura Roslin", "Lee Adama", new Cylon()}); + Observable.from(crewOfBattlestarGalactica) + .x(new ConvertToCylonDetector()) + .beep(new Func1(){ + @Override + public Boolean call(Object t) { + return t instanceof Cylon; + }}) + .boop(new Func1() { + @Override + public Jail call(Object cylon) { + return new Jail(cylon); + }}) + .DESTROY() + .x(new ConvertToObservable()) + .reduce("Cylon Detector finished. Report:\n", new Func2() { + @Override + public String call(String a, String n) { + return a + n + "\n"; + }}) + .subscribe(subscriber); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + } + + @Test + public void testConvertToConcurrentQueue() { + final AtomicReference thrown = new AtomicReference(null); + final AtomicBoolean isFinished = new AtomicBoolean(false); + ConcurrentLinkedQueue queue = Observable.range(0,5) + .flatMap(new Func1>(){ + @Override + public Observable call(final Integer i) { + return Observable.range(0, 5) + .observeOn(Schedulers.io()) + .map(new Func1(){ + @Override + public Integer call(Integer k) { + try { + Thread.sleep(System.currentTimeMillis() % 100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return i + k; + }}); + }}) + .x(new Func1, ConcurrentLinkedQueue>() { + @Override + public ConcurrentLinkedQueue call(OnSubscribe onSubscribe) { + final ConcurrentLinkedQueue q = new ConcurrentLinkedQueue(); + onSubscribe.call(new Subscriber(){ + @Override + public void onCompleted() { + isFinished.set(true); + } + + @Override + public void onError(Throwable e) { + thrown.set(e); + } + + @Override + public void onNext(Integer t) { + q.add(t); + }}); + return q; + }}); + + int x = 0; + while(!isFinished.get()) { + Integer i = queue.poll(); + if (i != null) { + x++; + System.out.println(x + " item: " + i); + } + } + assertEquals(null, thrown.get()); + } +} diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index 5f1667deb6..55e43896d3 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -53,7 +53,6 @@ import rx.functions.Func0; import rx.functions.Func1; import rx.functions.Func2; -import rx.functions.Functions; import rx.observables.ConnectableObservable; import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; @@ -1157,4 +1156,19 @@ public void testForEachWithNull() { // .forEach(null); } + + @Test + public void testExtend() { + final TestSubscriber subscriber = new TestSubscriber(); + final Object value = new Object(); + Observable.just(value).x(new Func1,Object>(){ + @Override + public Object call(OnSubscribe onSubscribe) { + onSubscribe.call(subscriber); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + subscriber.assertValue(value); + return subscriber.getOnNextEvents().get(0); + }}); + } } From ad1fbc2d108eccaa21ba11975dda8b25e4446fea Mon Sep 17 00:00:00 2001 From: David Gross Date: Fri, 7 Aug 2015 15:59:03 -0700 Subject: [PATCH 358/857] eliminate javadoc compiler warnings, add "since" stub --- src/main/java/rx/Observable.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 246e54f023..79825d622b 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -110,12 +110,13 @@ public interface Operator extends Func1, Subscriber< } /** - * Passes all emitted values from {@code this} Observable to the provided {@link ConversionFunc} to be - * collected and returned as a single value. Note that it is legal for a {@link ConversionFunc} to - * return an Observable (enabling chaining). + * Passes all emitted values from this Observable to the provided conversion function to be collected and + * returned as a single value. Note that it is legal for a conversion function to return an Observable + * (enabling chaining). * * @param conversion a function that converts from this {@code Observable} to an {@code R} - * @return an instance of R created by the provided Conversion + * @return an instance of R created by the provided conversion function + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public R x(Func1, ? extends R> conversion) { From f6ea890eabfbcaf925df4e5014d5055d9a6add1e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Sat, 8 Aug 2015 01:47:25 +0200 Subject: [PATCH 359/857] FromIterable overhead reduction. --- .../operators/OnSubscribeFromIterable.java | 105 ++++++++++-------- .../java/rx/operators/FromIterablePerf.java | 85 ++++++++++++++ 2 files changed, 142 insertions(+), 48 deletions(-) create mode 100644 src/perf/java/rx/operators/FromIterablePerf.java diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index 2aad771b57..f4790e75bd 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -16,11 +16,10 @@ package rx.internal.operators; import java.util.Iterator; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Producer; -import rx.Subscriber; /** * Converts an {@code Iterable} sequence into an {@code Observable}. @@ -50,14 +49,12 @@ public void call(final Subscriber o) { o.setProducer(new IterableProducer(o, it)); } - private static final class IterableProducer implements Producer { + private static final class IterableProducer extends AtomicLong implements Producer { + /** */ + private static final long serialVersionUID = -8730475647105475802L; private final Subscriber o; private final Iterator it; - private volatile long requested = 0; - @SuppressWarnings("rawtypes") - private static final AtomicLongFieldUpdater REQUESTED_UPDATER = AtomicLongFieldUpdater.newUpdater(IterableProducer.class, "requested"); - private IterableProducer(Subscriber o, Iterator it) { this.o = o; this.it = it; @@ -65,18 +62,41 @@ private IterableProducer(Subscriber o, Iterator it) { @Override public void request(long n) { - if (requested == Long.MAX_VALUE) { + if (get() == Long.MAX_VALUE) { // already started with fast-path return; } - if (n == Long.MAX_VALUE && REQUESTED_UPDATER.compareAndSet(this, 0, Long.MAX_VALUE)) { - // fast-path without backpressure + if (n == Long.MAX_VALUE && compareAndSet(0, Long.MAX_VALUE)) { + fastpath(); + } else + if (n > 0 && BackpressureUtils.getAndAddRequest(this, n) == 0L) { + slowpath(n); + } + + } + + void slowpath(long n) { + // backpressure is requested + final Subscriber o = this.o; + 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) { if (o.isUnsubscribed()) { return; } else if (it.hasNext()) { - o.onNext(it.next()); + if (--numToEmit >= 0) { + o.onNext(it.next()); + } else + break; } else if (!o.isUnsubscribed()) { o.onCompleted(); return; @@ -85,45 +105,34 @@ public void request(long n) { return; } } - } else if (n > 0) { - // backpressure is requested - long _c = BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER, this, n); - if (_c == 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 r = requested; - long numToEmit = r; - while (true) { - if (o.isUnsubscribed()) { - return; - } else if (it.hasNext()) { - if (--numToEmit >= 0) { - o.onNext(it.next()); - } else - break; - } else if (!o.isUnsubscribed()) { - o.onCompleted(); - return; - } else { - // is unsubscribed - return; - } - } - if (REQUESTED_UPDATER.addAndGet(this, -r) == 0) { - // we're done emitting the number requested so - // return - return; - } - - } + r = addAndGet(-r); + if (r == 0L) { + // we're done emitting the number requested so + // return + return; } + } + } + void fastpath() { + // fast-path without backpressure + final Subscriber o = this.o; + final Iterator it = this.it; + + while (true) { + if (o.isUnsubscribed()) { + return; + } else if (it.hasNext()) { + o.onNext(it.next()); + } else if (!o.isUnsubscribed()) { + o.onCompleted(); + return; + } else { + // is unsubscribed + return; + } + } } } diff --git a/src/perf/java/rx/operators/FromIterablePerf.java b/src/perf/java/rx/operators/FromIterablePerf.java new file mode 100644 index 0000000000..1368fcbf30 --- /dev/null +++ b/src/perf/java/rx/operators/FromIterablePerf.java @@ -0,0 +1,85 @@ +/** + * 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.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import rx.*; +import rx.internal.operators.OnSubscribeFromIterable; +import rx.jmh.LatchedObserver; + +/** + * Benchmark from(Iterable). + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*FromIterablePerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class FromIterablePerf { + Observable from; + OnSubscribeFromIterable direct; + @Param({"1", "1000", "1000000"}) + public int size; + + @Setup + public void setup() { + Integer[] array = new Integer[size]; + for (int i = 0; i < size; i++) { + array[i] = i; + } + from = Observable.from(Arrays.asList(array)); + direct = new OnSubscribeFromIterable(Arrays.asList(array)); + } + + @Benchmark + public void from(Blackhole bh) { + from.subscribe(new LatchedObserver(bh)); + } + @Benchmark + public void fromUnsafe(final Blackhole bh) { + from.unsafeSubscribe(createSubscriber(bh)); + } + + @Benchmark + public void direct(final Blackhole bh) { + direct.call(createSubscriber(bh)); + } + + Subscriber createSubscriber(final Blackhole bh) { + return new Subscriber() { + @Override + public void onNext(Integer t) { + bh.consume(t); + } + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + @Override + public void onCompleted() { + + } + }; + } +} From 54bb5881198714b84cefccc1b9c70d3f8c567078 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Sun, 9 Aug 2015 11:25:00 +0200 Subject: [PATCH 360/857] Correct scheduler memory leak test for from(Executor) and added check for periodic tasks retention as well. --- .../schedulers/CachedThreadSchedulerTest.java | 63 +++---------- .../schedulers/ComputationSchedulerTests.java | 17 ++++ .../rx/schedulers/ExecutorSchedulerTest.java | 89 +++++++++++++++++-- 3 files changed, 110 insertions(+), 59 deletions(-) diff --git a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java b/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java index 2b22e78068..9abb52b7ec 100644 --- a/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java +++ b/src/test/java/rx/schedulers/CachedThreadSchedulerTest.java @@ -16,18 +16,13 @@ package rx.schedulers; -import java.lang.management.*; -import java.util.concurrent.*; - -import junit.framework.Assert; +import static org.junit.Assert.assertTrue; import org.junit.Test; -import rx.Observable; -import rx.Scheduler; +import rx.*; +import rx.Scheduler.Worker; import rx.functions.*; -import rx.internal.schedulers.NewThreadWorker; -import static org.junit.Assert.assertTrue; public class CachedThreadSchedulerTest extends AbstractSchedulerConcurrencyTests { @@ -74,49 +69,17 @@ public final void testHandledErrorIsNotDeliveredToThreadHandler() throws Interru @Test(timeout = 30000) public void testCancelledTaskRetention() 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); - - Scheduler.Worker w = Schedulers.io().createWorker(); - for (int i = 0; i < 750000; i++) { - if (i % 50000 == 0) { - System.out.println(" -> still scheduling: " + i); - } - w.schedule(Actions.empty(), 1, TimeUnit.DAYS); + Worker w = Schedulers.io().createWorker(); + try { + ExecutorSchedulerTest.testCancelledRetention(w, false); + } finally { + w.unsubscribe(); } - - 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) { - Assert.fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d)); + w = Schedulers.io().createWorker(); + try { + ExecutorSchedulerTest.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 881224cfac..7191f60015 100644 --- a/src/test/java/rx/schedulers/ComputationSchedulerTests.java +++ b/src/test/java/rx/schedulers/ComputationSchedulerTests.java @@ -26,6 +26,7 @@ import rx.Observable; import rx.Scheduler; +import rx.Scheduler.Worker; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; @@ -151,4 +152,20 @@ public final void testUnhandledErrorIsDeliveredToThreadHandler() throws Interrup public final void testHandledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { SchedulerTests.testHandledErrorIsNotDeliveredToThreadHandler(getScheduler()); } + + @Test(timeout = 30000) + public void testCancelledTaskRetention() throws InterruptedException { + Worker w = Schedulers.computation().createWorker(); + try { + ExecutorSchedulerTest.testCancelledRetention(w, false); + } finally { + w.unsubscribe(); + } + w = Schedulers.computation().createWorker(); + try { + ExecutorSchedulerTest.testCancelledRetention(w, true); + } finally { + w.unsubscribe(); + } + } } diff --git a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java index cdefabc757..ed4e03213d 100644 --- a/src/test/java/rx/schedulers/ExecutorSchedulerTest.java +++ b/src/test/java/rx/schedulers/ExecutorSchedulerTest.java @@ -48,8 +48,8 @@ public final void testUnhandledErrorIsDeliveredToThreadHandler() throws Interrup public final void testHandledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { SchedulerTests.testHandledErrorIsNotDeliveredToThreadHandler(getScheduler()); } - @Test(timeout = 30000) - public void testCancelledTaskRetention() throws InterruptedException { + + public static void testCancelledRetention(Scheduler.Worker w, boolean periodic) throws InterruptedException { System.out.println("Wait before GC"); Thread.sleep(1000); @@ -64,13 +64,32 @@ public void testCancelledTaskRetention() throws InterruptedException { long initial = memHeap.getUsed(); System.out.printf("Starting: %.3f MB%n", initial / 1024.0 / 1024.0); - - Scheduler.Worker w = Schedulers.io().createWorker(); - for (int i = 0; i < 500000; i++) { - if (i % 50000 == 0) { - System.out.println(" -> still scheduling: " + i); + + 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); } - w.schedule(Actions.empty(), 1, TimeUnit.DAYS); } memHeap = memoryMXBean.getHeapMemoryUsage(); @@ -95,7 +114,30 @@ public void testCancelledTaskRetention() throws InterruptedException { 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(); + Scheduler s = Schedulers.from(exec); + try { + Scheduler.Worker w = s.createWorker(); + try { + testCancelledRetention(w, false); + } finally { + w.unsubscribe(); + } + + w = s.createWorker(); + try { + testCancelledRetention(w, true); + } finally { + w.unsubscribe(); + } + } finally { + exec.shutdownNow(); + } + } + /** A simple executor which queues tasks and executes them one-by-one if executeOne() is called. */ static final class TestExecutor implements Executor { final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); @@ -204,4 +246,33 @@ public void execute(Runnable command) { assertFalse(w.tasks.hasSubscriptions()); } + + @Test + public void testNoPeriodicTimedTaskPartRetention() throws InterruptedException { + Executor e = new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + ExecutorSchedulerWorker w = (ExecutorSchedulerWorker)Schedulers.from(e).createWorker(); + + final CountDownLatch cdl = new CountDownLatch(1); + final Action0 action = new Action0() { + @Override + public void call() { + cdl.countDown(); + } + }; + + Subscription s = w.schedulePeriodically(action, 0, 1, TimeUnit.DAYS); + + assertTrue(w.tasks.hasSubscriptions()); + + cdl.await(); + + s.unsubscribe(); + + assertFalse(w.tasks.hasSubscriptions()); + } } From b96c6a6fe723120ffeb472e7ad5c3ba3dd25ae42 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Mon, 10 Aug 2015 19:18:03 +0300 Subject: [PATCH 361/857] Fix for BackpressureUtils method javadoc --- src/main/java/rx/internal/operators/BackpressureUtils.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index c62eefcbbc..505b248553 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -61,9 +61,7 @@ public static long getAndAddRequest(AtomicLongFieldUpdater requested, T o * {@code requested} field to {@code Long.MAX_VALUE}. * * @param requested - * atomic field updater for a request count - * @param object - * contains the field updated by the updater + * atomic long that should be updated * @param n * the number of requests to add to the requested count * @return requested value just prior to successful addition From 620981ad1e57eac5d13b94ac5cde4078660ffac4 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Mon, 10 Aug 2015 19:51:03 +0300 Subject: [PATCH 362/857] Remove redundant cast in Exceptions --- src/main/java/rx/exceptions/Exceptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index 8ac7091def..b8907bf436 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -77,7 +77,7 @@ public static void throwIfFatal(Throwable t) { if (t instanceof OnErrorNotImplementedException) { throw (OnErrorNotImplementedException) t; } else if (t instanceof OnErrorFailedException) { - Throwable cause = ((OnErrorFailedException) t).getCause(); + Throwable cause = t.getCause(); if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else { From f3ce7238ced24349247c25ed2a76357f03f4eaf5 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Tue, 11 Aug 2015 02:15:48 +0300 Subject: [PATCH 363/857] Remove unnecessary static modifier --- src/main/java/rx/Notification.java | 2 +- src/main/java/rx/Single.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/Notification.java b/src/main/java/rx/Notification.java index 95d557726a..17a23d1031 100644 --- a/src/main/java/rx/Notification.java +++ b/src/main/java/rx/Notification.java @@ -162,7 +162,7 @@ public void accept(Observer observer) { } } - public static enum Kind { + public enum Kind { OnNext, OnError, OnCompleted } diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index ede27c8eb6..7fbf369b79 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -147,7 +147,7 @@ public final static Single create(OnSubscribe f) { /** * Invoked when Single.execute is called. */ - public static interface OnSubscribe extends Action1> { + public interface OnSubscribe extends Action1> { // cover for generics insanity } @@ -235,7 +235,7 @@ public Single compose(Transformer transformer) { * * @warn more complete description needed */ - public static interface Transformer extends Func1, Single> { + public interface Transformer extends Func1, Single> { // cover for generics insanity } From 9643d94e5f5c8815d00a3fd4003e52cff70cda70 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 11 Aug 2015 16:39:29 +0200 Subject: [PATCH 364/857] Range overhead reduction --- .../internal/operators/OnSubscribeRange.java | 112 +++++++++++------- .../java/rx/operators/OperatorRangePerf.java | 17 +-- .../operators/OnSubscribeRangeTest.java | 19 +++ 3 files changed, 92 insertions(+), 56 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRange.java b/src/main/java/rx/internal/operators/OnSubscribeRange.java index bcfbe0736b..383d17f28f 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRange.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRange.java @@ -15,11 +15,10 @@ */ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; +import rx.*; import rx.Observable.OnSubscribe; -import rx.Producer; -import rx.Subscriber; /** * Emit ints from start to end inclusive. @@ -39,13 +38,13 @@ public void call(final Subscriber o) { o.setProducer(new RangeProducer(o, start, end)); } - private static final class RangeProducer implements Producer { + private static final class RangeProducer extends AtomicLong implements Producer { + /** */ + private static final long serialVersionUID = 4114392207069098388L; + private final Subscriber o; - // accessed by REQUESTED_UPDATER - private volatile long requested; - private static final AtomicLongFieldUpdater REQUESTED_UPDATER = AtomicLongFieldUpdater.newUpdater(RangeProducer.class, "requested"); - private long index; private final int end; + private long index; private RangeProducer(Subscriber o, int start, int end) { this.o = o; @@ -55,54 +54,79 @@ private RangeProducer(Subscriber o, int start, int end) { @Override public void request(long n) { - if (requested == Long.MAX_VALUE) { + if (get() == Long.MAX_VALUE) { // already started with fast-path return; } - if (n == Long.MAX_VALUE && REQUESTED_UPDATER.compareAndSet(this, 0, Long.MAX_VALUE)) { + if (n == Long.MAX_VALUE && compareAndSet(0L, Long.MAX_VALUE)) { // fast-path without backpressure - for (long i = index; i <= end; i++) { + fastpath(); + } else if (n > 0L) { + long c = BackpressureUtils.getAndAddRequest(this, n); + if (c == 0L) { + // backpressure is requested + slowpath(n); + } + } + } + + /** + * + */ + void slowpath(long r) { + long idx = index; + while (true) { + /* + * This complicated logic is done to avoid touching the volatile `index` and `requested` values + * during the loop itself. If they are touched during the loop the performance is impacted significantly. + */ + long fs = end - idx + 1; + long e = Math.min(fs, r); + final boolean complete = fs <= r; + + fs = e + idx; + final Subscriber o = this.o; + + for (long i = idx; i != fs; i++) { if (o.isUnsubscribed()) { return; } o.onNext((int) i); } - if (!o.isUnsubscribed()) { + + if (complete) { + if (o.isUnsubscribed()) { + return; + } o.onCompleted(); + return; } - } else if (n > 0) { - // backpressure is requested - long _c = BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER,this, n); - if (_c == 0) { - while (true) { - /* - * This complicated logic is done to avoid touching the volatile `index` and `requested` values - * during the loop itself. If they are touched during the loop the performance is impacted significantly. - */ - long r = requested; - long idx = index; - long numLeft = end - idx + 1; - long e = Math.min(numLeft, r); - boolean completeOnFinish = numLeft <= r; - long stopAt = e + idx; - for (long i = idx; i < stopAt; i++) { - if (o.isUnsubscribed()) { - return; - } - o.onNext((int) i); - } - index = stopAt; - - if (completeOnFinish) { - o.onCompleted(); - return; - } - if (REQUESTED_UPDATER.addAndGet(this, -e) == 0) { - // we're done emitting the number requested so return - return; - } - } + + idx = fs; + index = fs; + + r = addAndGet(-e); + if (r == 0L) { + // we're done emitting the number requested so return + return; + } + } + } + + /** + * + */ + void fastpath() { + final long end = this.end + 1L; + final Subscriber o = this.o; + for (long i = index; i != end; i++) { + if (o.isUnsubscribed()) { + return; } + o.onNext((int) i); + } + if (!o.isUnsubscribed()) { + o.onCompleted(); } } } diff --git a/src/perf/java/rx/operators/OperatorRangePerf.java b/src/perf/java/rx/operators/OperatorRangePerf.java index 85ca76e46d..52fd0af7ff 100644 --- a/src/perf/java/rx/operators/OperatorRangePerf.java +++ b/src/perf/java/rx/operators/OperatorRangePerf.java @@ -17,18 +17,11 @@ 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.annotations.*; import org.openjdk.jmh.infra.Blackhole; -import rx.Observable; -import rx.Subscriber; +import rx.*; +import rx.internal.operators.OnSubscribeRange; @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @@ -50,7 +43,7 @@ public static class InputUsingRequest { @Setup public void setup(final Blackhole bh) { - observable = Observable.range(0, size); + observable = Observable.create(new OnSubscribeRange(0, size)); this.bh = bh; } @@ -98,7 +91,7 @@ public static class InputWithoutRequest { @Setup public void setup(final Blackhole bh) { - observable = Observable.range(0, size); + observable = Observable.create(new OnSubscribeRange(0, size)); this.bh = bh; } diff --git a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java index 6d4d97d019..acc2f6ff75 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRangeTest.java @@ -249,4 +249,23 @@ public void onNext(Integer t) { }}); assertTrue(completed.get()); } + + @Test(timeout = 1000) + public void testNearMaxValueWithoutBackpressure() { + TestSubscriber ts = TestSubscriber.create(); + Observable.range(Integer.MAX_VALUE - 1, 2).subscribe(ts); + + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertValues(Integer.MAX_VALUE - 1, Integer.MAX_VALUE); + } + @Test(timeout = 1000) + public void testNearMaxValueWithBackpressure() { + TestSubscriber ts = TestSubscriber.create(3); + Observable.range(Integer.MAX_VALUE - 1, 2).subscribe(ts); + + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertValues(Integer.MAX_VALUE - 1, Integer.MAX_VALUE); + } } From 6e8651e24dbcf23477f3dd52ca0f603d2926128f Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Mon, 10 Aug 2015 19:08:13 +0300 Subject: [PATCH 365/857] Remove redundant final modifier from static method in Actions --- src/main/java/rx/functions/Actions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/functions/Actions.java b/src/main/java/rx/functions/Actions.java index 342cfd030c..862bba221c 100644 --- a/src/main/java/rx/functions/Actions.java +++ b/src/main/java/rx/functions/Actions.java @@ -24,7 +24,7 @@ private Actions() { } @SuppressWarnings("unchecked") - public static final EmptyAction empty() { + public static EmptyAction empty() { return EMPTY_ACTION; } From adfabec8fb740fc873ece843736246742fdaba7c Mon Sep 17 00:00:00 2001 From: Steve Gury Date: Wed, 12 Aug 2015 15:32:25 -0700 Subject: [PATCH 366/857] Version 1.0.14 --- CHANGES.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 3f6776cd46..5a443e1a37 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,34 @@ # RxJava Releases # +### Version 1.0.14 – August 12th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.14%7C)) ### + +* [Pull 2963] (https://github.com/ReactiveX/RxJava/pull/2963) Set of standard producers and updated queue implementations +* [Pull 3138] (https://github.com/ReactiveX/RxJava/pull/3138) Range overhead reduction. +* [Pull 3137] (https://github.com/ReactiveX/RxJava/pull/3137) FromIterable overhead reduction. +* [Pull 3078] (https://github.com/ReactiveX/RxJava/pull/3078) switchOnNext() - fix lost requests race condition +* [Pull 3112] (https://github.com/ReactiveX/RxJava/pull/3112) Observers package test coverage and fixes. +* [Pull 3123] (https://github.com/ReactiveX/RxJava/pull/3123) Remove redundant type parameter in EmptyAction +* [Pull 3104] (https://github.com/ReactiveX/RxJava/pull/3104) Fix SynchronizedQueue.equals +* [Pull 3147] (https://github.com/ReactiveX/RxJava/pull/3147) Remove unnecessary static modifier +* [Pull 3144] (https://github.com/ReactiveX/RxJava/pull/3144) Remove redundant cast in Exceptions +* [Pull 3143] (https://github.com/ReactiveX/RxJava/pull/3143) Fix for BackpressureUtils method javadoc +* [Pull 3141] (https://github.com/ReactiveX/RxJava/pull/3141) Improved Scheduler.Worker memory leak detection +* [Pull 3082] (https://github.com/ReactiveX/RxJava/pull/3082) Observable.x(ConversionFunc) to allow extensions to Observables +* [Pull 3103] (https://github.com/ReactiveX/RxJava/pull/3103) materialize() - add backpressure support +* [Pull 3129] (https://github.com/ReactiveX/RxJava/pull/3129) Fix retry with predicate ignoring backpressure. +* [Pull 3121] (https://github.com/ReactiveX/RxJava/pull/3121) Improve performance of NewThreadWorker, disable search for setRemoveOnCancelPolicy() on Android API < 21 +* [Pull 3120] (https://github.com/ReactiveX/RxJava/pull/3120) No InterruptedException with synchronous BlockingObservable +* [Pull 3117] (https://github.com/ReactiveX/RxJava/pull/3117) Operator replay() now supports backpressure +* [Pull 3116] (https://github.com/ReactiveX/RxJava/pull/3116) cache() now supports backpressure +* [Pull 3110] (https://github.com/ReactiveX/RxJava/pull/3110) Test coverage of rx.functions utility methods. +* [Pull 3101] (https://github.com/ReactiveX/RxJava/pull/3101) Fix take swallowing exception if thrown by exactly the nth onNext call to it. +* [Pull 3109] (https://github.com/ReactiveX/RxJava/pull/3109) Unit tests and cleanup of JCTools' queues. +* [Pull 3108] (https://github.com/ReactiveX/RxJava/pull/3108) remove OperatorOnErrorFlatMap because unused +* [Pull 3079] (https://github.com/ReactiveX/RxJava/pull/3079) fix forEach javadoc +* [Pull 3085] (https://github.com/ReactiveX/RxJava/pull/3085) break tests as approach timeout so that don't fail on slow machines +* [Pull 3086] (https://github.com/ReactiveX/RxJava/pull/3086) improve ExecutorSchedulerTest.testOnBackpressureDrop +* [Pull 3093] (https://github.com/ReactiveX/RxJava/pull/3093) Fix request != 0 checking in the scalar paths of merge() + ### Version 1.0.13 – July 20th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.13%7C)) ### This release has quite a few bug fixes and some new functionality. Items of note are detailed here with the list of changes at the bottom. From 883e8fceadb3585df909e93e4868f653b28fe9c2 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 13 Aug 2015 00:49:23 +0200 Subject: [PATCH 367/857] Schedulers shutdown capability. --- .../schedulers/EventLoopsScheduler.java | 65 +++++++-- .../GenericScheduledExecutorService.java | 65 +++++++-- .../internal/schedulers/NewThreadWorker.java | 2 + .../schedulers/SchedulerLifecycle.java | 20 +++ .../java/rx/internal/util/ObjectPool.java | 76 ++++++---- .../java/rx/internal/util/RxRingBuffer.java | 8 +- .../rx/schedulers/CachedThreadScheduler.java | 132 +++++++++++++----- .../java/rx/schedulers/ExecutorScheduler.java | 2 +- src/main/java/rx/schedulers/Schedulers.java | 53 ++++++- .../schedulers/SchedulerLifecycleTest.java | 127 +++++++++++++++++ 10 files changed, 456 insertions(+), 94 deletions(-) rename src/main/java/rx/{ => internal}/schedulers/GenericScheduledExecutorService.java (58%) create mode 100644 src/main/java/rx/internal/schedulers/SchedulerLifecycle.java create mode 100644 src/test/java/rx/internal/schedulers/SchedulerLifecycleTest.java diff --git a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java index 986ea6d467..d901304680 100644 --- a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java @@ -16,13 +16,14 @@ package rx.internal.schedulers; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; import rx.*; import rx.functions.Action0; import rx.internal.util.*; import rx.subscriptions.*; -public class EventLoopsScheduler extends Scheduler { +public class EventLoopsScheduler extends Scheduler implements SchedulerLifecycle { /** Manages a fixed number of workers. */ private static final String THREAD_NAME_PREFIX = "RxComputationThreadPool-"; private static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); @@ -44,40 +45,82 @@ public class EventLoopsScheduler extends Scheduler { } MAX_THREADS = max; } + + static final PoolWorker SHUTDOWN_WORKER; + static { + SHUTDOWN_WORKER = new PoolWorker(new RxThreadFactory("RxComputationShutdown-")); + SHUTDOWN_WORKER.unsubscribe(); + } + static final class FixedSchedulerPool { final int cores; final PoolWorker[] eventLoops; long n; - FixedSchedulerPool() { + FixedSchedulerPool(int maxThreads) { // initialize event loops - this.cores = MAX_THREADS; - this.eventLoops = new PoolWorker[cores]; - for (int i = 0; i < cores; i++) { + this.cores = maxThreads; + this.eventLoops = new PoolWorker[maxThreads]; + for (int i = 0; i < maxThreads; i++) { this.eventLoops[i] = new PoolWorker(THREAD_FACTORY); } } public PoolWorker getEventLoop() { + int c = cores; + if (c == 0) { + return SHUTDOWN_WORKER; + } // simple round robin, improvements to come - return eventLoops[(int)(n++ % cores)]; + return eventLoops[(int)(n++ % c)]; + } + + public void shutdown() { + for (PoolWorker w : eventLoops) { + w.unsubscribe(); + } } } + /** This will indicate no pool is active. */ + static final FixedSchedulerPool NONE = new FixedSchedulerPool(0); - final FixedSchedulerPool pool; + final AtomicReference pool; /** * Create a scheduler with pool size equal to the available processor * count and using least-recent worker selection policy. */ public EventLoopsScheduler() { - pool = new FixedSchedulerPool(); + this.pool = new AtomicReference(NONE); + start(); } @Override public Worker createWorker() { - return new EventLoopWorker(pool.getEventLoop()); + return new EventLoopWorker(pool.get().getEventLoop()); + } + + @Override + public void start() { + FixedSchedulerPool update = new FixedSchedulerPool(MAX_THREADS); + if (!pool.compareAndSet(NONE, update)) { + update.shutdown(); + } + } + + @Override + public void shutdown() { + for (;;) { + FixedSchedulerPool curr = pool.get(); + if (curr == NONE) { + return; + } + if (pool.compareAndSet(curr, NONE)) { + curr.shutdown(); + return; + } + } } /** @@ -87,7 +130,7 @@ public Worker createWorker() { * @return the subscription */ public Subscription scheduleDirect(Action0 action) { - PoolWorker pw = pool.getEventLoop(); + PoolWorker pw = pool.get().getEventLoop(); return pw.scheduleActual(action, -1, TimeUnit.NANOSECONDS); } @@ -137,4 +180,4 @@ private static final class PoolWorker extends NewThreadWorker { super(threadFactory); } } -} +} \ No newline at end of file diff --git a/src/main/java/rx/schedulers/GenericScheduledExecutorService.java b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java similarity index 58% rename from src/main/java/rx/schedulers/GenericScheduledExecutorService.java rename to src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java index ca133275e7..e4c3e9ba61 100644 --- a/src/main/java/rx/schedulers/GenericScheduledExecutorService.java +++ b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java @@ -13,13 +13,14 @@ * 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.AtomicReference; import rx.Scheduler; -import rx.internal.schedulers.NewThreadWorker; import rx.internal.util.RxThreadFactory; - -import java.util.concurrent.*; +import rx.schedulers.*; /** * A default {@link ScheduledExecutorService} that can be used for scheduling actions when a {@link Scheduler} implementation doesn't have that ability. @@ -30,15 +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}. */ -/* package */final class GenericScheduledExecutorService { +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 final static GenericScheduledExecutorService INSTANCE = new GenericScheduledExecutorService(); - private final ScheduledExecutorService executor; - + /* Schedulers needs acces to this in order to work with the lifecycle. */ + public final static GenericScheduledExecutorService INSTANCE = new GenericScheduledExecutorService(); + + private final AtomicReference executor; + + static final ScheduledExecutorService NONE; + static { + NONE = Executors.newScheduledThreadPool(0); + NONE.shutdownNow(); + } + private GenericScheduledExecutorService() { + executor = new AtomicReference(NONE); + start(); + } + + @Override + public void start() { int count = Runtime.getRuntime().availableProcessors(); if (count > 4) { count = count / 2; @@ -47,21 +62,41 @@ private GenericScheduledExecutorService() { if (count > 8) { count = 8; } + ScheduledExecutorService exec = Executors.newScheduledThreadPool(count, THREAD_FACTORY); - if (!NewThreadWorker.tryEnableCancelPolicy(exec)) { - if (exec instanceof ScheduledThreadPoolExecutor) { - NewThreadWorker.registerExecutor((ScheduledThreadPoolExecutor)exec); + if (executor.compareAndSet(NONE, exec)) { + if (!NewThreadWorker.tryEnableCancelPolicy(exec)) { + if (exec instanceof ScheduledThreadPoolExecutor) { + NewThreadWorker.registerExecutor((ScheduledThreadPoolExecutor)exec); + } } + return; + } else { + exec.shutdownNow(); } - executor = exec; } - + + @Override + public void shutdown() { + for (;;) { + ScheduledExecutorService exec = executor.get(); + if (exec == NONE) { + return; + } + if (executor.compareAndSet(exec, NONE)) { + NewThreadWorker.deregisterExecutor(exec); + exec.shutdownNow(); + return; + } + } + } + /** * See class Javadoc for information on what this is for and how to use. * * @return {@link ScheduledExecutorService} for generic use. */ public static ScheduledExecutorService getInstance() { - return INSTANCE.executor; + return INSTANCE.executor.get(); } -} +} \ No newline at end of file diff --git a/src/main/java/rx/internal/schedulers/NewThreadWorker.java b/src/main/java/rx/internal/schedulers/NewThreadWorker.java index 4c47936871..0103a609ff 100644 --- a/src/main/java/rx/internal/schedulers/NewThreadWorker.java +++ b/src/main/java/rx/internal/schedulers/NewThreadWorker.java @@ -82,6 +82,8 @@ public void run() { }, PURGE_FREQUENCY, PURGE_FREQUENCY, TimeUnit.MILLISECONDS); break; + } else { + exec.shutdownNow(); } } while (true); diff --git a/src/main/java/rx/internal/schedulers/SchedulerLifecycle.java b/src/main/java/rx/internal/schedulers/SchedulerLifecycle.java new file mode 100644 index 0000000000..a9c7bd3f12 --- /dev/null +++ b/src/main/java/rx/internal/schedulers/SchedulerLifecycle.java @@ -0,0 +1,20 @@ +package rx.internal.schedulers; + +/** + * Represents the capability of a Scheduler to be start or shut down its maintained + * threads. + */ +public interface SchedulerLifecycle { + /** + * Allows the Scheduler instance to start threads + * and accept tasks on them. + *

Implementations should make sure the call is idempotent and threadsafe. + */ + void start(); + /** + * Instructs the Scheduler instance to stop threads + * and stop accepting tasks on any outstanding Workers. + *

Implementations should make sure the call is idempotent and threadsafe. + */ + void shutdown(); +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/ObjectPool.java b/src/main/java/rx/internal/util/ObjectPool.java index 8a059068a8..504c10cad4 100644 --- a/src/main/java/rx/internal/util/ObjectPool.java +++ b/src/main/java/rx/internal/util/ObjectPool.java @@ -18,20 +18,22 @@ package rx.internal.util; import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; -import rx.Scheduler; +import rx.Scheduler.Worker; import rx.functions.Action0; -import rx.internal.util.unsafe.MpmcArrayQueue; -import rx.internal.util.unsafe.UnsafeAccess; +import rx.internal.schedulers.SchedulerLifecycle; +import rx.internal.util.unsafe.*; import rx.schedulers.Schedulers; -public abstract class ObjectPool { +public abstract class ObjectPool implements SchedulerLifecycle { private Queue pool; + private final int minSize; private final int maxSize; + private final long validationInterval; - private Scheduler.Worker schedulerWorker; + private final AtomicReference schedulerWorker; public ObjectPool() { this(0, 0, 67); @@ -50,31 +52,14 @@ public ObjectPool() { * When the number of objects is greater than maxIdle, too many instances will be removed. */ private ObjectPool(final int min, final int max, final long validationInterval) { + this.minSize = min; this.maxSize = max; + this.validationInterval = validationInterval; + this.schedulerWorker = new AtomicReference(); // initialize pool initialize(min); - schedulerWorker = Schedulers.computation().createWorker(); - schedulerWorker.schedulePeriodically(new Action0() { - - @Override - public void call() { - int size = pool.size(); - if (size < min) { - int sizeToBeAdded = max - size; - for (int i = 0; i < sizeToBeAdded; i++) { - pool.add(createObject()); - } - } else if (size > max) { - int sizeToBeRemoved = size - max; - for (int i = 0; i < sizeToBeRemoved; i++) { - // pool.pollLast(); - pool.poll(); - } - } - } - - }, validationInterval, validationInterval, TimeUnit.SECONDS); + start(); } /** @@ -109,10 +94,43 @@ public void returnObject(T object) { /** * Shutdown this pool. */ + @Override public void shutdown() { - schedulerWorker.unsubscribe(); + Worker w = schedulerWorker.getAndSet(null); + if (w != null) { + w.unsubscribe(); + } } + @Override + public void start() { + Worker w = Schedulers.computation().createWorker(); + if (schedulerWorker.compareAndSet(null, w)) { + w.schedulePeriodically(new Action0() { + + @Override + public void call() { + int size = pool.size(); + if (size < minSize) { + int sizeToBeAdded = maxSize - size; + for (int i = 0; i < sizeToBeAdded; i++) { + pool.add(createObject()); + } + } else if (size > maxSize) { + int sizeToBeRemoved = size - maxSize; + for (int i = 0; i < sizeToBeRemoved; i++) { + // pool.pollLast(); + pool.poll(); + } + } + } + + }, validationInterval, validationInterval, TimeUnit.SECONDS); + } else { + w.unsubscribe(); + } + } + /** * Creates a new object. * diff --git a/src/main/java/rx/internal/util/RxRingBuffer.java b/src/main/java/rx/internal/util/RxRingBuffer.java index f038b2deec..5f35c7f6e5 100644 --- a/src/main/java/rx/internal/util/RxRingBuffer.java +++ b/src/main/java/rx/internal/util/RxRingBuffer.java @@ -276,7 +276,8 @@ public static RxRingBuffer getSpmcInstance() { } public static final int SIZE = _size; - private static ObjectPool> SPSC_POOL = new ObjectPool>() { + /* Public so Schedulers can manage the lifecycle of the inner worker. */ + public static ObjectPool> SPSC_POOL = new ObjectPool>() { @Override protected SpscArrayQueue createObject() { @@ -285,7 +286,8 @@ protected SpscArrayQueue createObject() { }; - private static ObjectPool> SPMC_POOL = new ObjectPool>() { + /* Public so Schedulers can manage the lifecycle of the inner worker. */ + public static ObjectPool> SPMC_POOL = new ObjectPool>() { @Override protected SpmcArrayQueue createObject() { @@ -452,4 +454,4 @@ public boolean isUnsubscribed() { return queue == null; } -} +} \ No newline at end of file diff --git a/src/main/java/rx/schedulers/CachedThreadScheduler.java b/src/main/java/rx/schedulers/CachedThreadScheduler.java index f1cd815b64..6ef56a17cb 100644 --- a/src/main/java/rx/schedulers/CachedThreadScheduler.java +++ b/src/main/java/rx/schedulers/CachedThreadScheduler.java @@ -15,19 +15,16 @@ */ package rx.schedulers; -import rx.Scheduler; -import rx.Subscription; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import rx.*; import rx.functions.Action0; -import rx.internal.schedulers.NewThreadWorker; -import rx.internal.schedulers.ScheduledAction; +import rx.internal.schedulers.*; import rx.internal.util.RxThreadFactory; -import rx.subscriptions.CompositeSubscription; -import rx.subscriptions.Subscriptions; - -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import rx.subscriptions.*; -/* package */final class CachedThreadScheduler extends Scheduler { +/* package */final class CachedThreadScheduler extends Scheduler implements SchedulerLifecycle { private static final String WORKER_THREAD_NAME_PREFIX = "RxCachedThreadScheduler-"; private static final RxThreadFactory WORKER_THREAD_FACTORY = new RxThreadFactory(WORKER_THREAD_NAME_PREFIX); @@ -36,31 +33,49 @@ private static final RxThreadFactory EVICTOR_THREAD_FACTORY = new RxThreadFactory(EVICTOR_THREAD_NAME_PREFIX); + private static final long KEEP_ALIVE_TIME = 60; + private static final TimeUnit KEEP_ALIVE_UNIT = TimeUnit.SECONDS; + + static final ThreadWorker SHUTDOWN_THREADWORKER; + static { + SHUTDOWN_THREADWORKER = new ThreadWorker(new RxThreadFactory("RxCachedThreadSchedulerShutdown-")); + SHUTDOWN_THREADWORKER.unsubscribe(); + } + private static final class CachedWorkerPool { private final long keepAliveTime; private final ConcurrentLinkedQueue expiringWorkerQueue; - private final ScheduledExecutorService evictExpiredWorkerExecutor; + private final CompositeSubscription allWorkers; + private final ScheduledExecutorService evictorService; + private final Future evictorTask; CachedWorkerPool(long keepAliveTime, TimeUnit unit) { - this.keepAliveTime = unit.toNanos(keepAliveTime); + this.keepAliveTime = unit != null ? unit.toNanos(keepAliveTime) : 0L; this.expiringWorkerQueue = new ConcurrentLinkedQueue(); - - evictExpiredWorkerExecutor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY); - evictExpiredWorkerExecutor.scheduleWithFixedDelay( - new Runnable() { - @Override - public void run() { - evictExpiredWorkers(); - } - }, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS - ); + this.allWorkers = new CompositeSubscription(); + + ScheduledExecutorService evictor = null; + Future task = null; + if (unit != null) { + evictor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY); + NewThreadWorker.tryEnableCancelPolicy(evictor); + task = evictor.scheduleWithFixedDelay( + new Runnable() { + @Override + public void run() { + evictExpiredWorkers(); + } + }, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS + ); + } + evictorService = evictor; + evictorTask = task; } - private static CachedWorkerPool INSTANCE = new CachedWorkerPool( - 60L, TimeUnit.SECONDS - ); - ThreadWorker get() { + if (allWorkers.isUnsubscribed()) { + return SHUTDOWN_THREADWORKER; + } while (!expiringWorkerQueue.isEmpty()) { ThreadWorker threadWorker = expiringWorkerQueue.poll(); if (threadWorker != null) { @@ -69,7 +84,9 @@ ThreadWorker get() { } // No cached worker found, so create a new one. - return new ThreadWorker(WORKER_THREAD_FACTORY); + ThreadWorker w = new ThreadWorker(WORKER_THREAD_FACTORY); + allWorkers.add(w); + return w; } void release(ThreadWorker threadWorker) { @@ -86,7 +103,7 @@ void evictExpiredWorkers() { for (ThreadWorker threadWorker : expiringWorkerQueue) { if (threadWorker.getExpirationTime() <= currentTimestamp) { if (expiringWorkerQueue.remove(threadWorker)) { - threadWorker.unsubscribe(); + allWorkers.remove(threadWorker); } } else { // Queue is ordered with the worker that will expire first in the beginning, so when we @@ -100,30 +117,79 @@ void evictExpiredWorkers() { long now() { return System.nanoTime(); } + + void shutdown() { + try { + if (evictorTask != null) { + evictorTask.cancel(true); + } + if (evictorService != null) { + evictorService.shutdownNow(); + } + } finally { + allWorkers.unsubscribe(); + } + } } + final AtomicReference pool; + + static final CachedWorkerPool NONE; + static { + NONE = new CachedWorkerPool(0, null); + NONE.shutdown(); + } + + public CachedThreadScheduler() { + this.pool = new AtomicReference(NONE); + start(); + } + + @Override + public void start() { + CachedWorkerPool update = new CachedWorkerPool(KEEP_ALIVE_TIME, KEEP_ALIVE_UNIT); + if (!pool.compareAndSet(NONE, update)) { + update.shutdown(); + } + } + @Override + public void shutdown() { + for (;;) { + CachedWorkerPool curr = pool.get(); + if (curr == NONE) { + return; + } + if (pool.compareAndSet(curr, NONE)) { + curr.shutdown(); + return; + } + } + } + @Override public Worker createWorker() { - return new EventLoopWorker(CachedWorkerPool.INSTANCE.get()); + return new EventLoopWorker(pool.get()); } private static final class EventLoopWorker extends Scheduler.Worker { private final CompositeSubscription innerSubscription = new CompositeSubscription(); + private final CachedWorkerPool pool; private final ThreadWorker threadWorker; @SuppressWarnings("unused") volatile int once; static final AtomicIntegerFieldUpdater ONCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(EventLoopWorker.class, "once"); - EventLoopWorker(ThreadWorker threadWorker) { - this.threadWorker = threadWorker; + EventLoopWorker(CachedWorkerPool pool) { + this.pool = pool; + this.threadWorker = pool.get(); } @Override public void unsubscribe() { if (ONCE_UPDATER.compareAndSet(this, 0, 1)) { // unsubscribe should be idempotent, so only do this once - CachedWorkerPool.INSTANCE.release(threadWorker); + pool.release(threadWorker); } innerSubscription.unsubscribe(); } @@ -168,4 +234,4 @@ public void setExpirationTime(long expirationTime) { this.expirationTime = expirationTime; } } -} +} \ 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 ce9643cf2b..d447400184 100644 --- a/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/src/main/java/rx/schedulers/ExecutorScheduler.java @@ -20,7 +20,7 @@ import rx.*; import rx.functions.Action0; -import rx.internal.schedulers.ScheduledAction; +import rx.internal.schedulers.*; import rx.plugins.RxJavaPlugins; import rx.subscriptions.*; diff --git a/src/main/java/rx/schedulers/Schedulers.java b/src/main/java/rx/schedulers/Schedulers.java index 8ded001e0e..2376f0fa8a 100644 --- a/src/main/java/rx/schedulers/Schedulers.java +++ b/src/main/java/rx/schedulers/Schedulers.java @@ -16,7 +16,8 @@ package rx.schedulers; import rx.Scheduler; -import rx.internal.schedulers.EventLoopsScheduler; +import rx.internal.schedulers.*; +import rx.internal.util.RxRingBuffer; import rx.plugins.RxJavaPlugins; import java.util.concurrent.Executor; @@ -137,4 +138,52 @@ public static TestScheduler test() { public static Scheduler from(Executor executor) { return new ExecutorScheduler(executor); } -} + + /** + * Starts those standard Schedulers which support the SchedulerLifecycle interface. + *

The operation is idempotent and threadsafe. + */ + public static void start() { + Schedulers s = INSTANCE; + synchronized (s) { + if (s.computationScheduler instanceof SchedulerLifecycle) { + ((SchedulerLifecycle) s.computationScheduler).start(); + } + if (s.ioScheduler instanceof SchedulerLifecycle) { + ((SchedulerLifecycle) s.ioScheduler).start(); + } + if (s.newThreadScheduler instanceof SchedulerLifecycle) { + ((SchedulerLifecycle) s.newThreadScheduler).start(); + } + GenericScheduledExecutorService.INSTANCE.start(); + + RxRingBuffer.SPSC_POOL.start(); + + RxRingBuffer.SPMC_POOL.start(); + } + } + /** + * Shuts down those standard Schedulers which support the SchedulerLifecycle interface. + *

The operation is idempotent and threadsafe. + */ + public static void shutdown() { + Schedulers s = INSTANCE; + synchronized (s) { + if (s.computationScheduler instanceof SchedulerLifecycle) { + ((SchedulerLifecycle) s.computationScheduler).shutdown(); + } + if (s.ioScheduler instanceof SchedulerLifecycle) { + ((SchedulerLifecycle) s.ioScheduler).shutdown(); + } + if (s.newThreadScheduler instanceof SchedulerLifecycle) { + ((SchedulerLifecycle) s.newThreadScheduler).shutdown(); + } + + GenericScheduledExecutorService.INSTANCE.shutdown(); + + RxRingBuffer.SPSC_POOL.shutdown(); + + RxRingBuffer.SPMC_POOL.shutdown(); + } + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/schedulers/SchedulerLifecycleTest.java b/src/test/java/rx/internal/schedulers/SchedulerLifecycleTest.java new file mode 100644 index 0000000000..1d6b1b6abc --- /dev/null +++ b/src/test/java/rx/internal/schedulers/SchedulerLifecycleTest.java @@ -0,0 +1,127 @@ +package rx.internal.schedulers; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.Test; + +import rx.Scheduler.Worker; +import rx.functions.Action0; +import rx.internal.util.RxRingBuffer; +import rx.schedulers.Schedulers; +import rx.subscriptions.CompositeSubscription; + +public class SchedulerLifecycleTest { + @Test + public void testShutdown() throws InterruptedException { + tryOutSchedulers(); + + System.out.println("testShutdown >> Giving time threads to spin-up"); + Thread.sleep(500); + + Set rxThreads = new HashSet(); + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("Rx")) { + rxThreads.add(t); + } + } + Schedulers.shutdown(); + System.out.println("testShutdown >> Giving time to threads to stop"); + Thread.sleep(500); + + StringBuilder b = new StringBuilder(); + for (Thread t : rxThreads) { + if (t.isAlive()) { + b.append("Thread " + t + " failed to shutdown\r\n"); + for (StackTraceElement ste : t.getStackTrace()) { + b.append(" ").append(ste).append("\r\n"); + } + } + } + if (b.length() > 0) { + System.out.print(b); + System.out.println("testShutdown >> Restarting schedulers..."); + Schedulers.start(); // restart them anyways + fail("Rx Threads were still alive:\r\n" + b); + } + + System.out.println("testShutdown >> Restarting schedulers..."); + Schedulers.start(); + + tryOutSchedulers(); + } + + private void tryOutSchedulers() throws InterruptedException { + final CountDownLatch cdl = new CountDownLatch(4); + + final Action0 countAction = new Action0() { + @Override + public void call() { + cdl.countDown(); + } + }; + + CompositeSubscription csub = new CompositeSubscription(); + + try { + Worker w1 = Schedulers.computation().createWorker(); + csub.add(w1); + w1.schedule(countAction); + + Worker w2 = Schedulers.io().createWorker(); + csub.add(w2); + w2.schedule(countAction); + + Worker w3 = Schedulers.newThread().createWorker(); + csub.add(w3); + w3.schedule(countAction); + + GenericScheduledExecutorService.getInstance().execute(new Runnable() { + @Override + public void run() { + countAction.call(); + } + }); + + RxRingBuffer.getSpscInstance().release(); + + if (!cdl.await(3, TimeUnit.SECONDS)) { + fail("countAction was not run by every worker"); + } + } finally { + csub.unsubscribe(); + } + } + + @Test + public void testStartIdempotence() throws InterruptedException { + tryOutSchedulers(); + + System.out.println("testStartIdempotence >> giving some time"); + Thread.sleep(500); + + Set rxThreads = new HashSet(); + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("Rx")) { + rxThreads.add(t); + System.out.println("testStartIdempotence >> " + t); + } + } + System.out.println("testStartIdempotence >> trying to start again"); + Schedulers.start(); + System.out.println("testStartIdempotence >> giving some time again"); + Thread.sleep(500); + + Set rxThreads2 = new HashSet(); + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("Rx")) { + rxThreads2.add(t); + System.out.println("testStartIdempotence >>>> " + t); + } + } + + assertEquals(rxThreads, rxThreads2); + } +} \ No newline at end of file From 9d7bd8bceff3fe4c0292632de0bbb353f9243216 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 20 Aug 2015 19:19:32 +0200 Subject: [PATCH 368/857] Fixed negative request due to unsubscription of a large requester --- .../rx/internal/operators/OperatorReplay.java | 2 +- .../operators/OperatorReplayTest.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index e1bf7aa352..b7b52aded3 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -501,7 +501,7 @@ void manageRequests() { InnerProducer[] a = producers.get(); long ri = maxChildRequested; - long maxTotalRequests = 0; + long maxTotalRequests = ri; for (InnerProducer rp : a) { maxTotalRequests = Math.max(maxTotalRequests, rp.totalRequested.get()); diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 046803b082..c0ec384d84 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -1120,4 +1120,29 @@ public void onNext(Integer t) { ts.assertNotCompleted(); ts.assertError(TestException.class); } + + @Test + public void unboundedLeavesEarly() { + PublishSubject source = PublishSubject.create(); + + final List requests = new ArrayList(); + + Observable out = source + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requests.add(t); + } + }).replay().autoConnect(); + + TestSubscriber ts1 = TestSubscriber.create(5); + TestSubscriber ts2 = TestSubscriber.create(10); + + out.subscribe(ts1); + out.subscribe(ts2); + ts2.unsubscribe(); + + Assert.assertEquals(Arrays.asList(5L, 5L), requests); + } + } \ No newline at end of file From 189928c79de2356fffa056e4a86521c200b4347c Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Aug 2015 20:56:25 -0700 Subject: [PATCH 369/857] Update README.md Add StackOverflow link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9014a45c7e..5dcb2199a8 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Learn more about RxJava on the ip = new InitialProducer(initialValue, child); + + Subscriber parent = new Subscriber() { private R value = initialValue; - boolean initialized = false; - @SuppressWarnings("unchecked") @Override public void onNext(T currentValue) { - emitInitialValueIfNeeded(child); - - if (this.value == NO_INITIAL_VALUE) { - // if there is NO_INITIAL_VALUE then we know it is type T for both so cast T to R - this.value = (R) currentValue; - } else { - try { - this.value = accumulator.call(this.value, currentValue); - } catch (Throwable e) { - Exceptions.throwIfFatal(e); - child.onError(OnErrorThrowable.addValueAsLastCause(e, currentValue)); - return; - } + R v = value; + try { + v = accumulator.call(v, currentValue); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + onError(OnErrorThrowable.addValueAsLastCause(e, currentValue)); + return; } - child.onNext(this.value); + value = v; + ip.onNext(v); } @Override public void onError(Throwable e) { - child.onError(e); + ip.onError(e); } @Override public void onCompleted() { - emitInitialValueIfNeeded(child); - child.onCompleted(); - } - - private void emitInitialValueIfNeeded(final Subscriber child) { - if (!initialized) { - initialized = true; - // we emit first time through if we have an initial value - if (initialValue != NO_INITIAL_VALUE) { - child.onNext(initialValue); - } - } + ip.onCompleted(); } - /** - * We want to adjust the requested value by subtracting 1 if we have an initial value - */ @Override public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - - final AtomicBoolean once = new AtomicBoolean(); - - final AtomicBoolean excessive = new AtomicBoolean(); - - @Override - public void request(long n) { - if (once.compareAndSet(false, true)) { - if (initialValue == NO_INITIAL_VALUE || n == Long.MAX_VALUE) { - producer.request(n); - } else if (n == 1) { - excessive.set(true); - producer.request(1); // request at least 1 - } else { - // n != Long.MAX_VALUE && n != 1 - producer.request(n - 1); - } - } else { - // pass-thru after first time - if (n > 1 // avoid to request 0 - && excessive.compareAndSet(true, false) && n != Long.MAX_VALUE) { - producer.request(n - 1); - } else { - producer.request(n); - } + ip.setProducer(producer); + } + }; + + child.add(parent); + child.setProducer(ip); + return parent; + } + + static final class InitialProducer implements Producer, Observer { + final Subscriber child; + final Queue queue; + + boolean emitting; + /** Missed a terminal event. */ + boolean missed; + /** Missed a request. */ + long missedRequested; + /** Missed a producer. */ + Producer missedProducer; + /** The current requested amount. */ + long requested; + /** The current producer. */ + Producer producer; + + volatile boolean done; + Throwable error; + + public InitialProducer(R initialValue, Subscriber child) { + this.child = child; + Queue q; + // TODO switch to the linked-array based queue once available + if (UnsafeAccess.isUnsafeAvailable()) { + q = new SpscLinkedQueue(); // new SpscUnboundedArrayQueue(8); + } else { + q = new SpscLinkedAtomicQueue(); // new SpscUnboundedAtomicArrayQueue(8); + } + this.queue = q; + q.offer(initialValue); + } + + @Override + public void request(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= required but it was " + n); + } else + if (n != 0L) { + synchronized (this) { + if (emitting) { + long mr = missedRequested; + long mu = mr + n; + if (mu < 0L) { + mu = Long.MAX_VALUE; } + missedRequested = mu; + return; } - }); + emitting = true; + } + + long r = requested; + long u = r + n; + if (u < 0L) { + u = Long.MAX_VALUE; + } + requested = u; + + Producer p = producer; + if (p != null) { + p.request(n); + } + + emitLoop(); } - }; + } + + @Override + public void onNext(R t) { + queue.offer(NotificationLite.instance().next(t)); + emit(); + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber child) { + if (child.isUnsubscribed()) { + return true; + } + if (d) { + Throwable err = error; + if (err != null) { + child.onError(err); + return true; + } else + if (empty) { + child.onCompleted(); + return true; + } + } + return false; + } + + @Override + public void onError(Throwable e) { + error = e; + done = true; + emit(); + } + + @Override + public void onCompleted() { + done = true; + emit(); + } + + public void setProducer(Producer p) { + if (p == null) { + throw new NullPointerException(); + } + synchronized (this) { + if (emitting) { + missedProducer = p; + return; + } + emitting = true; + } + producer = p; + long r = requested; + if (r != 0L) { + p.request(r); + } + emitLoop(); + } + + void emit() { + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + } + emitLoop(); + } + + void emitLoop() { + final Subscriber child = this.child; + final Queue queue = this.queue; + final NotificationLite nl = NotificationLite.instance(); + long r = requested; + for (;;) { + boolean max = r == Long.MAX_VALUE; + boolean d = done; + boolean empty = queue.isEmpty(); + if (checkTerminated(d, empty, child)) { + return; + } + while (r != 0L) { + d = done; + Object o = queue.poll(); + empty = o == null; + if (checkTerminated(d, empty, child)) { + return; + } + if (empty) { + break; + } + R v = nl.getValue(o); + try { + child.onNext(v); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + child.onError(OnErrorThrowable.addValueAsLastCause(e, v)); + return; + } + if (!max) { + r--; + } + } + if (!max) { + requested = r; + } + + Producer p; + long mr; + synchronized (this) { + p = missedProducer; + mr = missedRequested; + if (!missed && p == null && mr == 0L) { + emitting = false; + return; + } + missed = false; + missedProducer = null; + missedRequested = 0L; + } + + if (mr != 0L && !max) { + long u = r + mr; + if (u < 0L) { + u = Long.MAX_VALUE; + } + requested = u; + r = u; + } + + if (p != null) { + producer = p; + if (r != 0L) { + p.request(r); + } + } else { + p = producer; + if (p != null && mr != 0L) { + p.request(mr); + } + } + } + } } } diff --git a/src/test/java/rx/internal/operators/OperatorScanTest.java b/src/test/java/rx/internal/operators/OperatorScanTest.java index e05d4d9bb1..ac7772753f 100644 --- a/src/test/java/rx/internal/operators/OperatorScanTest.java +++ b/src/test/java/rx/internal/operators/OperatorScanTest.java @@ -15,33 +15,23 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; +import java.util.*; +import java.util.concurrent.atomic.*; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import org.mockito.MockitoAnnotations; +import rx.*; import rx.Observable; import rx.Observer; -import rx.Producer; -import rx.Subscriber; -import rx.functions.Action2; -import rx.functions.Func0; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.functions.*; +import rx.observables.AbstractOnSubscribe; import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; public class OperatorScanTest { @@ -360,4 +350,45 @@ public void onNext(Integer integer) { verify(producer.get(), never()).request(0); verify(producer.get(), times(2)).request(1); } + + @Test + public void testInitialValueEmittedNoProducer() { + PublishSubject source = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + source.scan(0, new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return t1 + t2; + } + }).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotCompleted(); + ts.assertValue(0); + } + + @Test + public void testInitialValueEmittedWithProducer() { + Observable source = new AbstractOnSubscribe() { + @Override + protected void next(rx.observables.AbstractOnSubscribe.SubscriptionState state) { + state.stop(); + } + }.toObservable(); + + TestSubscriber ts = TestSubscriber.create(); + + source.scan(0, new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return t1 + t2; + } + }).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotCompleted(); + ts.assertValue(0); + } } From 1cad3e6c4e359cff77ada616ba6ceb13483239ba Mon Sep 17 00:00:00 2001 From: akarnokd Date: Sat, 22 Aug 2015 12:35:40 +0200 Subject: [PATCH 371/857] BackpressureUtils capped add/multiply methods + tests --- .../internal/operators/BackpressureUtils.java | 43 ++++++++++++++----- .../operators/BackpressureUtilsTest.java | 39 +++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 src/test/java/rx/internal/operators/BackpressureUtilsTest.java diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index 505b248553..937f186535 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -44,11 +44,7 @@ public static long getAndAddRequest(AtomicLongFieldUpdater requested, T o // add n to field but check for overflow while (true) { long current = requested.get(object); - long next = current + n; - // check for overflow - if (next < 0) { - next = Long.MAX_VALUE; - } + long next = addCap(current, n); if (requested.compareAndSet(object, current, next)) { return current; } @@ -70,14 +66,41 @@ public static long getAndAddRequest(AtomicLong requested, long n) { // add n to field but check for overflow while (true) { long current = requested.get(); - long next = current + n; - // check for overflow - if (next < 0) { - next = Long.MAX_VALUE; - } + long next = addCap(current, n); if (requested.compareAndSet(current, next)) { return current; } } } + + /** + * Multiplies two positive longs and caps the result at Long.MAX_VALUE. + * @param a the first value + * @param b the second value + * @return the capped product of a and b + */ + public static long multiplyCap(long a, long b) { + long u = a * b; + if (((a | b) >>> 31) != 0) { + if (b != 0L && (u / b != a)) { + u = Long.MAX_VALUE; + } + } + return u; + } + + /** + * Adds two positive longs and caps the result at Long.MAX_VALUE. + * @param a the first value + * @param b the second value + * @return the capped sum of a and b + */ + public static long addCap(long a, long b) { + long u = a + b; + if (u < 0L) { + u = Long.MAX_VALUE; + } + return u; + } + } diff --git a/src/test/java/rx/internal/operators/BackpressureUtilsTest.java b/src/test/java/rx/internal/operators/BackpressureUtilsTest.java new file mode 100644 index 0000000000..0bc7f542bf --- /dev/null +++ b/src/test/java/rx/internal/operators/BackpressureUtilsTest.java @@ -0,0 +1,39 @@ +/** + * Copyright 2015 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 org.junit.Test; +import static org.junit.Assert.*; + +public class BackpressureUtilsTest { + @Test + public void testAddCap() { + assertEquals(2L, BackpressureUtils.addCap(1, 1)); + assertEquals(Long.MAX_VALUE, BackpressureUtils.addCap(1, Long.MAX_VALUE - 1)); + assertEquals(Long.MAX_VALUE, BackpressureUtils.addCap(1, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureUtils.addCap(Long.MAX_VALUE - 1, Long.MAX_VALUE - 1)); + assertEquals(Long.MAX_VALUE, BackpressureUtils.addCap(Long.MAX_VALUE, Long.MAX_VALUE)); + } + + @Test + public void testMultiplyCap() { + assertEquals(6, BackpressureUtils.multiplyCap(2, 3)); + assertEquals(Long.MAX_VALUE, BackpressureUtils.multiplyCap(2, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureUtils.multiplyCap(Long.MAX_VALUE, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureUtils.multiplyCap(1L << 32, 1L << 32)); + + } +} From 5fec06fdfec66a6a6422b18a65fd9d934d4fd7fd Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Fri, 14 Aug 2015 21:07:33 +1000 Subject: [PATCH 372/857] catch onCompleted unsubscribe error and report to RxJavaPlugin error handler --- .../OnCompletedFailedException.java | 29 +++++ .../UnsubscribeFailedException.java | 30 +++++ .../rx/internal/util/RxJavaPluginUtils.java | 40 +++++++ .../java/rx/observers/SafeSubscriber.java | 58 +++------- .../java/rx/observers/SafeObserverTest.java | 27 ++--- .../java/rx/observers/SafeSubscriberTest.java | 105 ++++++++++++++++-- 6 files changed, 223 insertions(+), 66 deletions(-) create mode 100644 src/main/java/rx/exceptions/OnCompletedFailedException.java create mode 100644 src/main/java/rx/exceptions/UnsubscribeFailedException.java create mode 100644 src/main/java/rx/internal/util/RxJavaPluginUtils.java diff --git a/src/main/java/rx/exceptions/OnCompletedFailedException.java b/src/main/java/rx/exceptions/OnCompletedFailedException.java new file mode 100644 index 0000000000..37632d86c6 --- /dev/null +++ b/src/main/java/rx/exceptions/OnCompletedFailedException.java @@ -0,0 +1,29 @@ +/** + * 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.exceptions; + +public final class OnCompletedFailedException extends RuntimeException { + + private static final long serialVersionUID = 8622579378868820554L; + + public OnCompletedFailedException(Throwable throwable) { + super(throwable); + } + + public OnCompletedFailedException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/src/main/java/rx/exceptions/UnsubscribeFailedException.java b/src/main/java/rx/exceptions/UnsubscribeFailedException.java new file mode 100644 index 0000000000..8b01df8aa3 --- /dev/null +++ b/src/main/java/rx/exceptions/UnsubscribeFailedException.java @@ -0,0 +1,30 @@ +/** + * 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.exceptions; + +public final class UnsubscribeFailedException extends RuntimeException { + + private static final long serialVersionUID = 4594672310593167598L; + + public UnsubscribeFailedException(Throwable throwable) { + super(throwable); + } + + public UnsubscribeFailedException(String message, Throwable throwable) { + super(message, throwable); + } + +} diff --git a/src/main/java/rx/internal/util/RxJavaPluginUtils.java b/src/main/java/rx/internal/util/RxJavaPluginUtils.java new file mode 100644 index 0000000000..b6b462412c --- /dev/null +++ b/src/main/java/rx/internal/util/RxJavaPluginUtils.java @@ -0,0 +1,40 @@ +/** + * 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.util; + +import rx.plugins.RxJavaPlugins; + +public final class RxJavaPluginUtils { + + public static void handleException(Throwable e) { + try { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } catch (Throwable pluginException) { + handlePluginException(pluginException); + } + } + + private static void handlePluginException(Throwable pluginException) { + /* + * We don't want errors from the plugin to affect normal flow. + * Since the plugin should never throw this is a safety net + * and will complain loudly to System.err so it gets fixed. + */ + System.err.println("RxJavaErrorHandler threw an Exception. It shouldn't. => " + pluginException.getMessage()); + pluginException.printStackTrace(); + } + +} diff --git a/src/main/java/rx/observers/SafeSubscriber.java b/src/main/java/rx/observers/SafeSubscriber.java index 0181887c34..8a9aad5179 100644 --- a/src/main/java/rx/observers/SafeSubscriber.java +++ b/src/main/java/rx/observers/SafeSubscriber.java @@ -20,9 +20,11 @@ import rx.Subscriber; import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; +import rx.exceptions.OnCompletedFailedException; import rx.exceptions.OnErrorFailedException; import rx.exceptions.OnErrorNotImplementedException; -import rx.plugins.RxJavaPlugins; +import rx.exceptions.UnsubscribeFailedException; +import rx.internal.util.RxJavaPluginUtils; /** * {@code SafeSubscriber} is a wrapper around {@code Subscriber} that ensures that the {@code Subscriber} @@ -83,11 +85,17 @@ public void onCompleted() { // we handle here instead of another method so we don't add stacks to the frame // which can prevent it from being able to handle StackOverflow Exceptions.throwIfFatal(e); - // handle errors if the onCompleted implementation fails, not just if the Observable fails - _onError(e); + RxJavaPluginUtils.handleException(e); + throw new OnCompletedFailedException(e.getMessage(), e); } finally { - // auto-unsubscribe - unsubscribe(); + try { + // Similarly to onError if failure occurs in unsubscribe then Rx contract is broken + // and we throw an UnsubscribeFailureException. + unsubscribe(); + } catch (Throwable e) { + RxJavaPluginUtils.handleException(e); + throw new UnsubscribeFailedException(e.getMessage(), e); + } } } } @@ -145,11 +153,7 @@ public void onNext(T args) { * @see the report of this bug */ protected void _onError(Throwable e) { - try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); - } catch (Throwable pluginException) { - handlePluginException(pluginException); - } + RxJavaPluginUtils.handleException(e); try { actual.onError(e); } catch (Throwable e2) { @@ -168,11 +172,7 @@ protected void _onError(Throwable e) { try { unsubscribe(); } catch (Throwable unsubscribeException) { - try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(unsubscribeException); - } catch (Throwable pluginException) { - handlePluginException(pluginException); - } + RxJavaPluginUtils.handleException(unsubscribeException); throw new RuntimeException("Observer.onError not implemented and error while unsubscribing.", new CompositeException(Arrays.asList(e, unsubscribeException))); } throw (OnErrorNotImplementedException) e2; @@ -182,19 +182,11 @@ protected void _onError(Throwable e) { * * https://github.com/ReactiveX/RxJava/issues/198 */ - try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e2); - } catch (Throwable pluginException) { - handlePluginException(pluginException); - } + RxJavaPluginUtils.handleException(e2); try { unsubscribe(); } catch (Throwable unsubscribeException) { - try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(unsubscribeException); - } catch (Throwable pluginException) { - handlePluginException(pluginException); - } + RxJavaPluginUtils.handleException(unsubscribeException); throw new OnErrorFailedException("Error occurred when trying to propagate error to Observer.onError and during unsubscription.", new CompositeException(Arrays.asList(e, e2, unsubscribeException))); } @@ -205,25 +197,11 @@ protected void _onError(Throwable e) { try { unsubscribe(); } catch (RuntimeException unsubscribeException) { - try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(unsubscribeException); - } catch (Throwable pluginException) { - handlePluginException(pluginException); - } + RxJavaPluginUtils.handleException(unsubscribeException); throw new OnErrorFailedException(unsubscribeException); } } - private void handlePluginException(Throwable pluginException) { - /* - * We don't want errors from the plugin to affect normal flow. - * Since the plugin should never throw this is a safety net - * and will complain loudly to System.err so it gets fixed. - */ - System.err.println("RxJavaErrorHandler threw an Exception. It shouldn't. => " + pluginException.getMessage()); - pluginException.printStackTrace(); - } - /** * Returns the {@link Subscriber} underlying this {@code SafeSubscriber}. * diff --git a/src/test/java/rx/observers/SafeObserverTest.java b/src/test/java/rx/observers/SafeObserverTest.java index 1083e995c7..7924bb4026 100644 --- a/src/test/java/rx/observers/SafeObserverTest.java +++ b/src/test/java/rx/observers/SafeObserverTest.java @@ -22,6 +22,7 @@ import org.junit.Test; +import junit.framework.Assert; import rx.Subscriber; import rx.exceptions.*; import rx.functions.Action0; @@ -68,19 +69,6 @@ public void onCompletedFailure() { } } - @Test - public void onCompletedFailureSafe() { - AtomicReference onError = new AtomicReference(); - try { - new SafeSubscriber(OBSERVER_ONCOMPLETED_FAIL(onError)).onCompleted(); - assertNotNull(onError.get()); - assertTrue(onError.get() instanceof SafeObserverTestException); - assertEquals("onCompletedFail", onError.get().getMessage()); - } catch (Exception e) { - fail("expects exception to be passed to onError"); - } - } - @Test public void onErrorFailure() { try { @@ -184,8 +172,8 @@ public void call() { e.printStackTrace(); assertTrue(o.isUnsubscribed()); - - assertTrue(e instanceof SafeObserverTestException); + assertTrue(e instanceof UnsubscribeFailedException); + assertTrue(e.getCause() instanceof SafeObserverTestException); assertEquals("failure from unsubscribe", e.getMessage()); // expected since onError fails so SafeObserver can't help } @@ -475,9 +463,12 @@ public void onCompleted() { } }); - s.onCompleted(); - - assertTrue("Error not received", error.get() instanceof TestException); + try { + s.onCompleted(); + Assert.fail(); + } catch (OnCompletedFailedException e) { + assertNull(error.get()); + } } @Test diff --git a/src/test/java/rx/observers/SafeSubscriberTest.java b/src/test/java/rx/observers/SafeSubscriberTest.java index 85c2d7b07f..5ce37cdea4 100644 --- a/src/test/java/rx/observers/SafeSubscriberTest.java +++ b/src/test/java/rx/observers/SafeSubscriberTest.java @@ -15,15 +15,25 @@ */ package rx.observers; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; -import org.junit.*; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; -import rx.exceptions.*; +import rx.exceptions.OnCompletedFailedException; +import rx.exceptions.OnErrorFailedException; +import rx.exceptions.OnErrorNotImplementedException; +import rx.exceptions.TestException; +import rx.exceptions.UnsubscribeFailedException; import rx.functions.Action0; -import rx.plugins.*; +import rx.plugins.RxJavaErrorHandler; +import rx.plugins.RxJavaPlugins; import rx.subscriptions.Subscriptions; public class SafeSubscriberTest { @@ -51,10 +61,12 @@ public void onCompleted() { } }; SafeSubscriber safe = new SafeSubscriber(ts); - - safe.onCompleted(); - - assertTrue(safe.isUnsubscribed()); + try { + safe.onCompleted(); + Assert.fail(); + } catch (OnCompletedFailedException e) { + assertTrue(safe.isUnsubscribed()); + } } @Test @@ -76,7 +88,7 @@ public void onCompleted() { assertTrue(safe.isUnsubscribed()); } - @Test + @Test(expected=OnCompletedFailedException.class) public void testPluginException() { RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { @Override @@ -227,4 +239,81 @@ public void call() { safe.onError(new TestException()); } + + @Test + public void testPluginErrorHandlerReceivesExceptionWhenUnsubscribeAfterCompletionThrows() { + final AtomicInteger calls = new AtomicInteger(); + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + @Override + public void handleError(Throwable e) { + calls.incrementAndGet(); + } + }); + + final AtomicInteger errors = new AtomicInteger(); + TestSubscriber ts = new TestSubscriber() { + @Override + public void onError(Throwable e) { + errors.incrementAndGet(); + } + }; + final RuntimeException ex = new RuntimeException(); + SafeSubscriber safe = new SafeSubscriber(ts); + safe.add(Subscriptions.create(new Action0() { + @Override + public void call() { + throw ex; + } + })); + + try { + safe.onCompleted(); + Assert.fail(); + } catch(UnsubscribeFailedException e) { + assertEquals(1, (int) calls.get()); + assertEquals(0, (int) errors.get()); + } + } + + @Test + public void testPluginErrorHandlerReceivesExceptionFromFailingUnsubscribeAfterCompletionThrows() { + final AtomicInteger calls = new AtomicInteger(); + RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + @Override + public void handleError(Throwable e) { + calls.incrementAndGet(); + } + }); + + final AtomicInteger errors = new AtomicInteger(); + TestSubscriber ts = new TestSubscriber() { + + @Override + public void onCompleted() { + throw new RuntimeException(); + } + + @Override + public void onError(Throwable e) { + errors.incrementAndGet(); + } + }; + SafeSubscriber safe = new SafeSubscriber(ts); + safe.add(Subscriptions.create(new Action0() { + @Override + public void call() { + throw new RuntimeException(); + } + })); + + try { + safe.onCompleted(); + Assert.fail(); + } catch(UnsubscribeFailedException e) { + assertEquals(2, (int) calls.get()); + assertEquals(0, (int) errors.get()); + } + } + + } From c70aa21a101411b9a89f253f11cda0f10615b112 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 24 Aug 2015 19:17:24 +0200 Subject: [PATCH 373/857] MapNotification producer NPE fix --- .../operators/OperatorMapNotification.java | 96 +++++++++++-------- .../OperatorMapNotificationTest.java | 55 +++++++++++ 2 files changed, 109 insertions(+), 42 deletions(-) create mode 100644 src/test/java/rx/internal/operators/OperatorMapNotificationTest.java diff --git a/src/main/java/rx/internal/operators/OperatorMapNotification.java b/src/main/java/rx/internal/operators/OperatorMapNotification.java index be363663fb..bb92f2c077 100644 --- a/src/main/java/rx/internal/operators/OperatorMapNotification.java +++ b/src/main/java/rx/internal/operators/OperatorMapNotification.java @@ -19,16 +19,12 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicLong; +import rx.*; import rx.Observable.Operator; -import rx.Producer; -import rx.Subscriber; -import rx.Subscription; -import rx.exceptions.MissingBackpressureException; -import rx.exceptions.OnErrorThrowable; -import rx.functions.Func0; -import rx.functions.Func1; -import rx.internal.util.unsafe.SpscArrayQueue; -import rx.internal.util.unsafe.UnsafeAccess; +import rx.exceptions.*; +import rx.functions.*; +import rx.internal.producers.ProducerArbiter; +import rx.internal.util.unsafe.*; /** * Applies a function of your choosing to every item emitted by an {@code Observable}, and emits the results of @@ -50,44 +46,60 @@ public OperatorMapNotification(Func1 onNext, Func1 call(final Subscriber o) { - Subscriber subscriber = new Subscriber() { - SingleEmitter emitter; - @Override - public void setProducer(Producer producer) { - emitter = new SingleEmitter(o, producer, this); - o.setProducer(emitter); - } - - @Override - public void onCompleted() { - try { - emitter.offerAndComplete(onCompleted.call()); - } catch (Throwable e) { - o.onError(e); - } - } + final ProducerArbiter pa = new ProducerArbiter(); + + MapNotificationSubscriber subscriber = new MapNotificationSubscriber(pa, o); + o.add(subscriber); + subscriber.init(); + return subscriber; + } + + final class MapNotificationSubscriber extends Subscriber { + private final Subscriber o; + private final ProducerArbiter pa; + final SingleEmitter emitter; + + private MapNotificationSubscriber(ProducerArbiter pa, Subscriber o) { + this.pa = pa; + this.o = o; + this.emitter = new SingleEmitter(o, pa, this); + } + + void init() { + o.setProducer(emitter); + } - @Override - public void onError(Throwable e) { - try { - emitter.offerAndComplete(onError.call(e)); - } catch (Throwable e2) { - o.onError(e); - } + @Override + public void setProducer(Producer producer) { + pa.setProducer(producer); + } + + @Override + public void onCompleted() { + try { + emitter.offerAndComplete(onCompleted.call()); + } catch (Throwable e) { + o.onError(e); } + } - @Override - public void onNext(T t) { - try { - emitter.offer(onNext.call(t)); - } catch (Throwable e) { - o.onError(OnErrorThrowable.addValueAsLastCause(e, t)); - } + @Override + public void onError(Throwable e) { + try { + emitter.offerAndComplete(onError.call(e)); + } catch (Throwable e2) { + o.onError(e); } + } - }; - o.add(subscriber); - return subscriber; + @Override + public void onNext(T t) { + try { + emitter.offer(onNext.call(t)); + } catch (Throwable e) { + o.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + } + } } static final class SingleEmitter extends AtomicLong implements Producer, Subscription { /** */ diff --git a/src/test/java/rx/internal/operators/OperatorMapNotificationTest.java b/src/test/java/rx/internal/operators/OperatorMapNotificationTest.java new file mode 100644 index 0000000000..2f1e603337 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorMapNotificationTest.java @@ -0,0 +1,55 @@ +/** + * 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 org.junit.Test; + +import rx.Observable; +import rx.functions.*; +import rx.observers.TestSubscriber; + +public class OperatorMapNotificationTest { + @Test + public void testJust() { + TestSubscriber ts = TestSubscriber.create(); + Observable.just(1) + .flatMap( + new Func1>() { + @Override + public Observable call(Integer item) { + return Observable.just((Object)(item + 1)); + } + }, + new Func1>() { + @Override + public Observable call(Throwable e) { + return Observable.error(e); + } + }, + new Func0>() { + @Override + public Observable call() { + return Observable.never(); + } + } + ).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotCompleted(); + ts.assertValue(2); + } +} From 3769be0047f605f511f380c58f17ddfb0137b138 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 25 Aug 2015 00:10:29 +0200 Subject: [PATCH 374/857] Refactored exception reporting of most operators. --- src/main/java/rx/exceptions/Exceptions.java | 29 +++++++++++++-- .../operators/OnSubscribeCombineLatest.java | 4 +- .../internal/operators/OnSubscribeDefer.java | 3 +- ...ubscribeDelaySubscriptionWithSelector.java | 3 +- .../operators/OnSubscribeGroupJoin.java | 25 +++++-------- .../internal/operators/OnSubscribeJoin.java | 19 ++++------ .../operators/OnSubscribeTimerOnce.java | 3 +- .../OnSubscribeTimerPeriodically.java | 5 ++- .../OnSubscribeToObservableFuture.java | 3 +- .../internal/operators/OnSubscribeUsing.java | 6 ++- .../rx/internal/operators/OperatorAll.java | 4 +- .../rx/internal/operators/OperatorAny.java | 7 +--- .../OperatorBufferWithSingleObservable.java | 7 ++-- .../operators/OperatorBufferWithSize.java | 5 ++- .../OperatorBufferWithStartEndObservable.java | 5 ++- .../operators/OperatorBufferWithTime.java | 9 +++-- .../rx/internal/operators/OperatorCast.java | 4 +- .../OperatorDebounceWithSelector.java | 3 +- .../operators/OperatorDebounceWithTime.java | 5 ++- .../operators/OperatorDelayWithSelector.java | 3 +- .../internal/operators/OperatorDoOnEach.java | 10 ++--- .../rx/internal/operators/OperatorFilter.java | 4 +- .../internal/operators/OperatorGroupBy.java | 6 +-- .../rx/internal/operators/OperatorMap.java | 4 +- .../operators/OperatorMapNotification.java | 6 +-- .../internal/operators/OperatorMapPair.java | 4 +- .../OperatorOnErrorResumeNextViaFunction.java | 2 +- .../operators/OperatorOnErrorReturn.java | 1 + .../internal/operators/OperatorPublish.java | 4 +- .../rx/internal/operators/OperatorReplay.java | 3 +- .../operators/OperatorSampleWithTime.java | 3 +- .../rx/internal/operators/OperatorSkip.java | 6 +-- .../operators/OperatorTakeLastOne.java | 3 +- .../operators/OperatorTakeUntilPredicate.java | 6 +-- .../internal/operators/OperatorTakeWhile.java | 9 ++--- .../OperatorTimeoutWithSelector.java | 6 +-- .../operators/OperatorToObservableList.java | 3 +- .../OperatorToObservableSortedList.java | 3 +- .../operators/OperatorWithLatestFrom.java | 3 +- .../rx/internal/operators/OperatorZip.java | 5 +-- .../operators/OperatorZipIterable.java | 6 ++- .../operators/TakeLastQueueProducer.java | 3 +- .../producers/ProducerObserverArbiter.java | 4 +- .../rx/internal/producers/QueuedProducer.java | 4 +- .../producers/QueuedValueProducer.java | 4 +- .../producers/SingleDelayedProducer.java | 4 +- .../rx/internal/producers/SingleProducer.java | 3 +- .../operators/OperatorFilterTest.java | 37 +++++++++++++++---- 48 files changed, 167 insertions(+), 141 deletions(-) diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index b8907bf436..1b29838637 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -15,10 +15,9 @@ */ package rx.exceptions; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; +import rx.Observer; import rx.annotations.Experimental; /** @@ -178,4 +177,28 @@ public static void throwIfAny(List exceptions) { "Multiple exceptions", exceptions); } } + + /** + * Forwards a fatal exception or reports it along with the value + * caused it to the given Observer. + * @param t the exception + * @param o the observer to report to + * @param value the value that caused the exception + */ + @Experimental + 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 + * @param o the observer to report to + * @param value the value that caused the exception + */ + @Experimental + public static void throwOrReport(Throwable t, Observer o) { + Exceptions.throwIfFatal(t); + o.onError(t); + } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java index 953895af32..54e1335205 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java @@ -23,9 +23,9 @@ import rx.Observable; import rx.Observable.OnSubscribe; +import rx.exceptions.*; import rx.Producer; import rx.Subscriber; -import rx.exceptions.MissingBackpressureException; import rx.functions.FuncN; import rx.internal.util.RxRingBuffer; @@ -202,7 +202,7 @@ public boolean onNext(int index, T t) { } catch (MissingBackpressureException e) { onError(e); } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, child); } } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeDefer.java b/src/main/java/rx/internal/operators/OnSubscribeDefer.java index 23ee937145..34a060df41 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDefer.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDefer.java @@ -18,6 +18,7 @@ import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.functions.Func0; import rx.observers.Subscribers; @@ -44,7 +45,7 @@ public void call(final Subscriber s) { try { o = observableFactory.call(); } catch (Throwable t) { - s.onError(t); + Exceptions.throwOrReport(t, s); return; } o.unsafeSubscribe(Subscribers.wrap(s)); diff --git a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java index b32179b3f7..8c57c44f62 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java +++ b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionWithSelector.java @@ -17,6 +17,7 @@ import rx.*; import rx.Observable.OnSubscribe; +import rx.exceptions.Exceptions; import rx.functions.Func0; import rx.observers.Subscribers; @@ -58,7 +59,7 @@ public void onNext(U t) { }); } catch (Throwable e) { - child.onError(e); + Exceptions.throwOrReport(e, child); } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java b/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java index e80b560dcd..4b7509c2d9 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java +++ b/src/main/java/rx/internal/operators/OnSubscribeGroupJoin.java @@ -15,24 +15,17 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import rx.*; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observer; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Func1; -import rx.functions.Func2; -import rx.observers.SerializedObserver; -import rx.observers.SerializedSubscriber; -import rx.subjects.PublishSubject; -import rx.subjects.Subject; -import rx.subscriptions.CompositeSubscription; -import rx.subscriptions.RefCountSubscription; +import rx.exceptions.Exceptions; +import rx.functions.*; +import rx.observers.*; +import rx.subjects.*; +import rx.subscriptions.*; /** * Corrrelates two sequences when they overlap and groups the results. @@ -192,7 +185,7 @@ public void onNext(T1 args) { } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); } } @@ -242,7 +235,7 @@ public void onNext(T2 args) { o.onNext(args); } } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeJoin.java b/src/main/java/rx/internal/operators/OnSubscribeJoin.java index b6edd5c366..f93437c5d0 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeJoin.java +++ b/src/main/java/rx/internal/operators/OnSubscribeJoin.java @@ -15,20 +15,15 @@ */ package rx.internal.operators; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import rx.*; import rx.Observable; import rx.Observable.OnSubscribe; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.exceptions.Exceptions; +import rx.functions.*; import rx.observers.SerializedSubscriber; -import rx.subscriptions.CompositeSubscription; -import rx.subscriptions.SerialSubscription; +import rx.subscriptions.*; /** * Correlates the elements of two sequences based on overlapping durations. @@ -154,7 +149,7 @@ public void onNext(TLeft args) { subscriber.onNext(result); } } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); } } @@ -266,7 +261,7 @@ public void onNext(TRight args) { } } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeTimerOnce.java b/src/main/java/rx/internal/operators/OnSubscribeTimerOnce.java index cf31ae6ca8..2b618ac21f 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeTimerOnce.java +++ b/src/main/java/rx/internal/operators/OnSubscribeTimerOnce.java @@ -19,6 +19,7 @@ import rx.Observable.OnSubscribe; import rx.Scheduler; import rx.Scheduler.Worker; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Action0; @@ -47,7 +48,7 @@ public void call() { try { child.onNext(0L); } catch (Throwable t) { - child.onError(t); + Exceptions.throwOrReport(t, child); return; } child.onCompleted(); diff --git a/src/main/java/rx/internal/operators/OnSubscribeTimerPeriodically.java b/src/main/java/rx/internal/operators/OnSubscribeTimerPeriodically.java index 33811b69e5..bbf14f34a7 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeTimerPeriodically.java +++ b/src/main/java/rx/internal/operators/OnSubscribeTimerPeriodically.java @@ -19,6 +19,7 @@ import rx.Observable.OnSubscribe; import rx.Scheduler; import rx.Scheduler.Worker; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Action0; @@ -51,9 +52,9 @@ public void call() { child.onNext(counter++); } catch (Throwable e) { try { - child.onError(e); - } finally { worker.unsubscribe(); + } finally { + Exceptions.throwOrReport(e, child); } } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java b/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java index 74adaff15b..72adcf5d50 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java +++ b/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java @@ -19,6 +19,7 @@ import java.util.concurrent.TimeUnit; import rx.Observable.OnSubscribe; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Action0; import rx.subscriptions.Subscriptions; @@ -83,7 +84,7 @@ public void call() { //refuse to emit onError if already unsubscribed return; } - subscriber.onError(e); + Exceptions.throwOrReport(e, subscriber); } } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeUsing.java b/src/main/java/rx/internal/operators/OnSubscribeUsing.java index 14d8d46b7b..4355e78221 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeUsing.java +++ b/src/main/java/rx/internal/operators/OnSubscribeUsing.java @@ -20,7 +20,7 @@ import rx.*; import rx.Observable.OnSubscribe; -import rx.exceptions.CompositeException; +import rx.exceptions.*; import rx.functions.*; import rx.observers.Subscribers; @@ -72,6 +72,8 @@ public void call(final Subscriber subscriber) { observable.unsafeSubscribe(Subscribers.wrap(subscriber)); } catch (Throwable e) { Throwable disposeError = disposeEagerlyIfRequested(disposeOnceOnly); + Exceptions.throwIfFatal(e); + Exceptions.throwIfFatal(disposeError); if (disposeError != null) subscriber.onError(new CompositeException(Arrays.asList(e, disposeError))); else @@ -80,7 +82,7 @@ public void call(final Subscriber subscriber) { } } catch (Throwable e) { // then propagate error - subscriber.onError(e); + Exceptions.throwOrReport(e, subscriber); } } diff --git a/src/main/java/rx/internal/operators/OperatorAll.java b/src/main/java/rx/internal/operators/OperatorAll.java index 96f0429d01..00845c7334 100644 --- a/src/main/java/rx/internal/operators/OperatorAll.java +++ b/src/main/java/rx/internal/operators/OperatorAll.java @@ -18,7 +18,6 @@ import rx.Observable.Operator; import rx.Subscriber; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; import rx.internal.producers.SingleDelayedProducer; @@ -47,8 +46,7 @@ public void onNext(T t) { try { result = predicate.call(t); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, this, t); return; } if (!result && !done) { diff --git a/src/main/java/rx/internal/operators/OperatorAny.java b/src/main/java/rx/internal/operators/OperatorAny.java index 7bd9d3f00b..ac84ec961f 100644 --- a/src/main/java/rx/internal/operators/OperatorAny.java +++ b/src/main/java/rx/internal/operators/OperatorAny.java @@ -16,11 +16,9 @@ package rx.internal.operators; -import rx.Observable; +import rx.*; import rx.Observable.Operator; -import rx.Subscriber; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; import rx.internal.producers.SingleDelayedProducer; @@ -51,8 +49,7 @@ public void onNext(T t) { try { result = predicate.call(t); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, this, t); return; } if (result && !done) { diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithSingleObservable.java b/src/main/java/rx/internal/operators/OperatorBufferWithSingleObservable.java index 204fc365f1..187bc0494a 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithSingleObservable.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithSingleObservable.java @@ -20,6 +20,7 @@ import rx.Observable; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.Observer; import rx.Subscriber; import rx.functions.Func0; @@ -79,7 +80,7 @@ public Subscriber call(final Subscriber> child) { try { closing = bufferClosingSelector.call(); } catch (Throwable t) { - child.onError(t); + Exceptions.throwOrReport(t, child); return Subscribers.empty(); } final BufferingSubscriber bsub = new BufferingSubscriber(new SerializedSubscriber>(child)); @@ -157,7 +158,7 @@ public void onCompleted() { } child.onNext(toEmit); } catch (Throwable t) { - child.onError(t); + Exceptions.throwOrReport(t, child); return; } child.onCompleted(); @@ -183,7 +184,7 @@ void emit() { } done = true; } - child.onError(t); + Exceptions.throwOrReport(t, child); } } } diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java index 60872b5ba7..d0bfdb1dbb 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java @@ -24,6 +24,7 @@ import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; +import rx.exceptions.Exceptions; /** * This operation takes @@ -118,7 +119,7 @@ public void onCompleted() { try { child.onNext(oldBuffer); } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); return; } } @@ -218,7 +219,7 @@ public void onCompleted() { try { child.onNext(chunk); } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); return; } } diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithStartEndObservable.java b/src/main/java/rx/internal/operators/OperatorBufferWithStartEndObservable.java index 8e8cb4eeef..328f401a2e 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithStartEndObservable.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithStartEndObservable.java @@ -21,6 +21,7 @@ import java.util.List; import rx.Observable; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.Observer; import rx.Subscriber; import rx.functions.Func1; @@ -145,7 +146,7 @@ public void onCompleted() { child.onNext(chunk); } } catch (Throwable t) { - child.onError(t); + Exceptions.throwOrReport(t, child); return; } child.onCompleted(); @@ -163,7 +164,7 @@ void startBuffer(TOpening v) { try { cobs = bufferClosing.call(v); } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); return; } Subscriber closeSubscriber = new Subscriber() { diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithTime.java b/src/main/java/rx/internal/operators/OperatorBufferWithTime.java index 3b2dd63704..bbb723d2b3 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithTime.java @@ -25,6 +25,7 @@ import rx.Scheduler; import rx.Scheduler.Worker; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.functions.Action0; import rx.observers.SerializedSubscriber; @@ -159,7 +160,7 @@ public void onCompleted() { child.onNext(chunk); } } catch (Throwable t) { - child.onError(t); + Exceptions.throwOrReport(t, child); return; } child.onCompleted(); @@ -208,7 +209,7 @@ void emitChunk(List chunkToEmit) { try { child.onNext(chunkToEmit); } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); } } } @@ -273,7 +274,7 @@ public void onCompleted() { } child.onNext(toEmit); } catch (Throwable t) { - child.onError(t); + Exceptions.throwOrReport(t, child); return; } child.onCompleted(); @@ -299,7 +300,7 @@ void emit() { try { child.onNext(toEmit); } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); } } } diff --git a/src/main/java/rx/internal/operators/OperatorCast.java b/src/main/java/rx/internal/operators/OperatorCast.java index 92dd1792e5..248fcb1970 100644 --- a/src/main/java/rx/internal/operators/OperatorCast.java +++ b/src/main/java/rx/internal/operators/OperatorCast.java @@ -16,8 +16,8 @@ package rx.internal.operators; import rx.Observable.Operator; +import rx.exceptions.*; import rx.Subscriber; -import rx.exceptions.OnErrorThrowable; /** * Converts the elements of an observable sequence to the specified type. @@ -49,7 +49,7 @@ public void onNext(T t) { try { o.onNext(castClass.cast(t)); } catch (Throwable e) { - onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, this, t); } } }; diff --git a/src/main/java/rx/internal/operators/OperatorDebounceWithSelector.java b/src/main/java/rx/internal/operators/OperatorDebounceWithSelector.java index c7ae83ff63..6be5ff2210 100644 --- a/src/main/java/rx/internal/operators/OperatorDebounceWithSelector.java +++ b/src/main/java/rx/internal/operators/OperatorDebounceWithSelector.java @@ -17,6 +17,7 @@ import rx.Observable; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Func1; import rx.internal.operators.OperatorDebounceWithTime.DebounceState; @@ -59,7 +60,7 @@ public void onNext(T t) { try { debouncer = selector.call(t); } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, this); return; } diff --git a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java index 45d3f14cd9..df7c451287 100644 --- a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java @@ -19,6 +19,7 @@ import rx.Observable.Operator; import rx.Scheduler; import rx.Scheduler.Worker; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Action0; import rx.observers.SerializedSubscriber; @@ -130,7 +131,7 @@ public void emit(int index, Subscriber onNextAndComplete, Subscriber onErr try { onNextAndComplete.onNext(localValue); } catch (Throwable e) { - onError.onError(e); + Exceptions.throwOrReport(e, onError, localValue); return; } @@ -166,7 +167,7 @@ public void emitAndComplete(Subscriber onNextAndComplete, Subscriber onErr try { onNextAndComplete.onNext(localValue); } catch (Throwable e) { - onError.onError(e); + Exceptions.throwOrReport(e, onError, localValue); return; } } diff --git a/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java b/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java index 16744563d7..1c4447c1d2 100644 --- a/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java +++ b/src/main/java/rx/internal/operators/OperatorDelayWithSelector.java @@ -17,6 +17,7 @@ import rx.Observable; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Func1; import rx.observers.SerializedSubscriber; @@ -71,7 +72,7 @@ public T call(V v) { })); } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, this); } } diff --git a/src/main/java/rx/internal/operators/OperatorDoOnEach.java b/src/main/java/rx/internal/operators/OperatorDoOnEach.java index 27c3309a1f..4b3e8d54cf 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnEach.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnEach.java @@ -15,11 +15,9 @@ */ package rx.internal.operators; +import rx.*; import rx.Observable.Operator; -import rx.Observer; -import rx.Subscriber; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; /** * Converts the elements of an observable sequence to the specified type. @@ -45,7 +43,7 @@ public void onCompleted() { try { doOnEachObserver.onCompleted(); } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, this); return; } // Set `done` here so that the error in `doOnEachObserver.onCompleted()` can be noticed by observer @@ -64,7 +62,7 @@ public void onError(Throwable e) { try { doOnEachObserver.onError(e); } catch (Throwable e2) { - observer.onError(e2); + Exceptions.throwOrReport(e2, observer); return; } observer.onError(e); @@ -78,7 +76,7 @@ public void onNext(T value) { try { doOnEachObserver.onNext(value); } catch (Throwable e) { - onError(OnErrorThrowable.addValueAsLastCause(e, value)); + Exceptions.throwOrReport(e, this, value); return; } observer.onNext(value); diff --git a/src/main/java/rx/internal/operators/OperatorFilter.java b/src/main/java/rx/internal/operators/OperatorFilter.java index 276d5f9765..2dbd827a94 100644 --- a/src/main/java/rx/internal/operators/OperatorFilter.java +++ b/src/main/java/rx/internal/operators/OperatorFilter.java @@ -17,7 +17,7 @@ import rx.Observable.Operator; import rx.Subscriber; -import rx.exceptions.OnErrorThrowable; +import rx.exceptions.*; import rx.functions.Func1; /** @@ -57,7 +57,7 @@ public void onNext(T t) { request(1); } } catch (Throwable e) { - child.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, child, t); } } diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 3d8f45067c..ffced4c923 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -26,10 +26,10 @@ import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observable.Operator; +import rx.exceptions.*; import rx.Observer; import rx.Producer; import rx.Subscriber; -import rx.exceptions.OnErrorThrowable; import rx.functions.Action0; import rx.functions.Func1; import rx.observables.GroupedObservable; @@ -226,7 +226,7 @@ public void onNext(T t) { emitItem(group, nl.next(t)); } } catch (Throwable e) { - onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, this, t); } } @@ -287,7 +287,7 @@ public void onNext(T t) { try { o.onNext(elementSelector.call(t)); } catch (Throwable e) { - onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, this, t); } } }); diff --git a/src/main/java/rx/internal/operators/OperatorMap.java b/src/main/java/rx/internal/operators/OperatorMap.java index 1f82a21764..5816887479 100644 --- a/src/main/java/rx/internal/operators/OperatorMap.java +++ b/src/main/java/rx/internal/operators/OperatorMap.java @@ -18,7 +18,6 @@ import rx.Observable.Operator; import rx.Subscriber; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; /** @@ -54,8 +53,7 @@ public void onNext(T t) { try { o.onNext(transformer.call(t)); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, this, t); } } diff --git a/src/main/java/rx/internal/operators/OperatorMapNotification.java b/src/main/java/rx/internal/operators/OperatorMapNotification.java index bb92f2c077..a0c0994032 100644 --- a/src/main/java/rx/internal/operators/OperatorMapNotification.java +++ b/src/main/java/rx/internal/operators/OperatorMapNotification.java @@ -79,7 +79,7 @@ public void onCompleted() { try { emitter.offerAndComplete(onCompleted.call()); } catch (Throwable e) { - o.onError(e); + Exceptions.throwOrReport(e, o); } } @@ -88,7 +88,7 @@ public void onError(Throwable e) { try { emitter.offerAndComplete(onError.call(e)); } catch (Throwable e2) { - o.onError(e); + Exceptions.throwOrReport(e2, o); } } @@ -97,7 +97,7 @@ public void onNext(T t) { try { emitter.offer(onNext.call(t)); } catch (Throwable e) { - o.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, o, t); } } } diff --git a/src/main/java/rx/internal/operators/OperatorMapPair.java b/src/main/java/rx/internal/operators/OperatorMapPair.java index af95ce1426..29848d2f78 100644 --- a/src/main/java/rx/internal/operators/OperatorMapPair.java +++ b/src/main/java/rx/internal/operators/OperatorMapPair.java @@ -17,8 +17,8 @@ import rx.Observable; import rx.Observable.Operator; +import rx.exceptions.*; import rx.Subscriber; -import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; import rx.functions.Func2; @@ -85,7 +85,7 @@ public R call(U inner) { } })); } catch (Throwable e) { - o.onError(OnErrorThrowable.addValueAsLastCause(e, outer)); + Exceptions.throwOrReport(e, o, outer); } } diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java index 70380a1a2b..5141a0974d 100644 --- a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java +++ b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java @@ -99,7 +99,7 @@ public void setProducer(Producer producer) { Observable resume = resumeFunction.call(e); resume.unsafeSubscribe(next); } catch (Throwable e2) { - child.onError(e2); + Exceptions.throwOrReport(e2, child); } } diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorReturn.java b/src/main/java/rx/internal/operators/OperatorOnErrorReturn.java index 8702093e6c..3830f591fd 100644 --- a/src/main/java/rx/internal/operators/OperatorOnErrorReturn.java +++ b/src/main/java/rx/internal/operators/OperatorOnErrorReturn.java @@ -78,6 +78,7 @@ public void onError(Throwable e) { T result = resultFunction.call(e); child.onNext(result); } catch (Throwable x) { + Exceptions.throwIfFatal(x); child.onError(new CompositeException(Arrays.asList(e, x))); return; } diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 492cd8f261..65cf83dd25 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -19,7 +19,7 @@ import java.util.concurrent.atomic.*; import rx.*; -import rx.exceptions.MissingBackpressureException; +import rx.exceptions.*; import rx.functions.*; import rx.internal.util.*; import rx.internal.util.unsafe.*; @@ -561,7 +561,7 @@ void dispatch() { } catch (Throwable t) { // we bounce back exceptions and kick out the child subscriber ip.unsubscribe(); - ip.child.onError(t); + Exceptions.throwOrReport(t, ip.child, value); continue; } // indicate this child has received 1 element diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index b7b52aded3..6b42f1fb51 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -63,8 +63,7 @@ public void call(final Subscriber child) { co = connectableFactory.call(); observable = selector.call(co); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - child.onError(e); + Exceptions.throwOrReport(e, child); return; } diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java index 7138d760d4..f3130cbb97 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java @@ -20,6 +20,7 @@ import rx.Observable.Operator; import rx.Scheduler; import rx.Scheduler.Worker; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Action0; import rx.observers.SerializedSubscriber; @@ -103,7 +104,7 @@ public void call() { T v = (T)localValue; subscriber.onNext(v); } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, this); } } } diff --git a/src/main/java/rx/internal/operators/OperatorSkip.java b/src/main/java/rx/internal/operators/OperatorSkip.java index 878898aaba..505c1491e7 100644 --- a/src/main/java/rx/internal/operators/OperatorSkip.java +++ b/src/main/java/rx/internal/operators/OperatorSkip.java @@ -15,11 +15,7 @@ */ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicBoolean; - -import rx.Observable; -import rx.Producer; -import rx.Subscriber; +import rx.*; /** * Returns an Observable that skips the first num items emitted by the source diff --git a/src/main/java/rx/internal/operators/OperatorTakeLastOne.java b/src/main/java/rx/internal/operators/OperatorTakeLastOne.java index a9bb7b5d33..a7998a1667 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLastOne.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLastOne.java @@ -3,6 +3,7 @@ import java.util.concurrent.atomic.AtomicInteger; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.Producer; import rx.Subscriber; @@ -150,7 +151,7 @@ private void emit() { try { child.onNext(t); } catch (Throwable e) { - child.onError(e); + Exceptions.throwOrReport(e, child); return; } } diff --git a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java index 668f049a99..c33fab0b47 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java @@ -15,11 +15,10 @@ */ package rx.internal.operators; -import rx.Observable.Operator; import rx.*; +import rx.Observable.Operator; import rx.annotations.Experimental; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; import rx.functions.Func1; /** @@ -47,8 +46,7 @@ public void onNext(T t) { stop = stopPredicate.call(t); } catch (Throwable e) { done = true; - Exceptions.throwIfFatal(e); - child.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, child, t); unsubscribe(); return; } diff --git a/src/main/java/rx/internal/operators/OperatorTakeWhile.java b/src/main/java/rx/internal/operators/OperatorTakeWhile.java index 7d7a219270..0c34df7b6f 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeWhile.java +++ b/src/main/java/rx/internal/operators/OperatorTakeWhile.java @@ -18,11 +18,9 @@ import rx.Observable.Operator; import rx.Subscriber; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.functions.*; -/** +/**O * Returns an Observable that emits items emitted by the source Observable as long as a specified * condition is true. *

@@ -60,8 +58,7 @@ public void onNext(T t) { isSelected = predicate.call(t, counter++); } catch (Throwable e) { done = true; - Exceptions.throwIfFatal(e); - subscriber.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, subscriber, t); unsubscribe(); return; } diff --git a/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java b/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java index ce201c3c26..eff265e4e5 100644 --- a/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java +++ b/src/main/java/rx/internal/operators/OperatorTimeoutWithSelector.java @@ -49,8 +49,7 @@ public Subscription call( try { o = firstTimeoutSelector.call(); } catch (Throwable t) { - Exceptions.throwIfFatal(t); - timeoutSubscriber.onError(t); + Exceptions.throwOrReport(t, timeoutSubscriber); return Subscriptions.unsubscribed(); } return o.unsafeSubscribe(new Subscriber() { @@ -85,8 +84,7 @@ public Subscription call( try { o = timeoutSelector.call(value); } catch (Throwable t) { - Exceptions.throwIfFatal(t); - timeoutSubscriber.onError(t); + Exceptions.throwOrReport(t, timeoutSubscriber); return Subscriptions.unsubscribed(); } return o.unsafeSubscribe(new Subscriber() { diff --git a/src/main/java/rx/internal/operators/OperatorToObservableList.java b/src/main/java/rx/internal/operators/OperatorToObservableList.java index e77826acc6..d2e9d717f6 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableList.java @@ -18,6 +18,7 @@ import java.util.*; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.*; import rx.internal.producers.SingleDelayedProducer; @@ -85,7 +86,7 @@ public void onCompleted() { */ result = new ArrayList(list); } catch (Throwable t) { - onError(t); + Exceptions.throwOrReport(t, this); return; } list = null; diff --git a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java index a3e9c54839..19246cbe7c 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java @@ -18,6 +18,7 @@ import java.util.*; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.*; import rx.functions.Func2; import rx.internal.producers.SingleDelayedProducer; @@ -75,7 +76,7 @@ public void onCompleted() { // sort the list before delivery Collections.sort(a, sortFunction); } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, this); return; } producer.setValue(a); diff --git a/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java b/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java index 4bf610b6b1..95a4c30561 100644 --- a/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java +++ b/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java @@ -19,6 +19,7 @@ import rx.*; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.functions.Func2; import rx.observers.SerializedSubscriber; @@ -58,7 +59,7 @@ public void onNext(T t) { s.onNext(result); } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, this); return; } } diff --git a/src/main/java/rx/internal/operators/OperatorZip.java b/src/main/java/rx/internal/operators/OperatorZip.java index 623731755a..d4f0560718 100644 --- a/src/main/java/rx/internal/operators/OperatorZip.java +++ b/src/main/java/rx/internal/operators/OperatorZip.java @@ -20,11 +20,10 @@ import rx.Observable; import rx.Observable.Operator; +import rx.exceptions.*; import rx.Observer; import rx.Producer; import rx.Subscriber; -import rx.exceptions.MissingBackpressureException; -import rx.exceptions.OnErrorThrowable; import rx.functions.Func2; import rx.functions.Func3; import rx.functions.Func4; @@ -265,7 +264,7 @@ void tick() { requested.decrementAndGet(); emitted++; } catch (Throwable e) { - child.onError(OnErrorThrowable.addValueAsLastCause(e, vs)); + Exceptions.throwOrReport(e, child, vs); return; } // now remove them diff --git a/src/main/java/rx/internal/operators/OperatorZipIterable.java b/src/main/java/rx/internal/operators/OperatorZipIterable.java index e73e093082..f913854d1d 100644 --- a/src/main/java/rx/internal/operators/OperatorZipIterable.java +++ b/src/main/java/rx/internal/operators/OperatorZipIterable.java @@ -18,6 +18,7 @@ import java.util.Iterator; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Func2; import rx.observers.Subscribers; @@ -41,7 +42,8 @@ public Subscriber call(final Subscriber subscriber) { return Subscribers.empty(); } } catch (Throwable e) { - subscriber.onError(e); + Exceptions.throwOrReport(e, subscriber); + return Subscribers.empty(); } return new Subscriber(subscriber) { boolean once; @@ -67,7 +69,7 @@ public void onNext(T1 t) { onCompleted(); } } catch (Throwable e) { - onError(e); + Exceptions.throwOrReport(e, this); } } diff --git a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java index 633d28ca66..7fc5ce9235 100644 --- a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java +++ b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java @@ -18,6 +18,7 @@ import rx.Producer; import rx.Subscriber; +import rx.exceptions.Exceptions; import java.util.Deque; import java.util.concurrent.atomic.AtomicLongFieldUpdater; @@ -75,7 +76,7 @@ void emit(long previousRequested) { notification.accept(subscriber, value); } } catch (Throwable e) { - subscriber.onError(e); + Exceptions.throwOrReport(e, subscriber); } finally { deque.clear(); } diff --git a/src/main/java/rx/internal/producers/ProducerObserverArbiter.java b/src/main/java/rx/internal/producers/ProducerObserverArbiter.java index ff059590b5..7600815094 100644 --- a/src/main/java/rx/internal/producers/ProducerObserverArbiter.java +++ b/src/main/java/rx/internal/producers/ProducerObserverArbiter.java @@ -233,9 +233,7 @@ void emitLoop() { try { c.onNext(v); } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - Throwable ex1 = OnErrorThrowable.addValueAsLastCause(ex, v); - c.onError(ex1); + Exceptions.throwOrReport(ex, c, v); return; } } diff --git a/src/main/java/rx/internal/producers/QueuedProducer.java b/src/main/java/rx/internal/producers/QueuedProducer.java index 8dbf4f361e..51747dd9b9 100644 --- a/src/main/java/rx/internal/producers/QueuedProducer.java +++ b/src/main/java/rx/internal/producers/QueuedProducer.java @@ -169,9 +169,7 @@ private void drain() { c.onNext(t); } } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - Throwable ex1 = OnErrorThrowable.addValueAsLastCause(ex, v != NULL_SENTINEL ? v : null); - c.onError(ex1); + Exceptions.throwOrReport(ex, c, v != NULL_SENTINEL ? v : null); return; } r--; diff --git a/src/main/java/rx/internal/producers/QueuedValueProducer.java b/src/main/java/rx/internal/producers/QueuedValueProducer.java index df61a05041..d165a412b7 100644 --- a/src/main/java/rx/internal/producers/QueuedValueProducer.java +++ b/src/main/java/rx/internal/producers/QueuedValueProducer.java @@ -117,9 +117,7 @@ private void drain() { c.onNext(t); } } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - Throwable ex1 = OnErrorThrowable.addValueAsLastCause(ex, v != NULL_SENTINEL ? v : null); - c.onError(ex1); + Exceptions.throwOrReport(ex, c, v != NULL_SENTINEL ? v : null); return; } if (c.isUnsubscribed()) { diff --git a/src/main/java/rx/internal/producers/SingleDelayedProducer.java b/src/main/java/rx/internal/producers/SingleDelayedProducer.java index 5da11dd80f..12403fe21b 100644 --- a/src/main/java/rx/internal/producers/SingleDelayedProducer.java +++ b/src/main/java/rx/internal/producers/SingleDelayedProducer.java @@ -101,9 +101,7 @@ private static void emit(Subscriber c, T v) { try { c.onNext(v); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - Throwable e1 = OnErrorThrowable.addValueAsLastCause(e, v); - c.onError(e1); + Exceptions.throwOrReport(e, c, v); return; } if (c.isUnsubscribed()) { diff --git a/src/main/java/rx/internal/producers/SingleProducer.java b/src/main/java/rx/internal/producers/SingleProducer.java index 8e8e17dcb4..337d815d91 100644 --- a/src/main/java/rx/internal/producers/SingleProducer.java +++ b/src/main/java/rx/internal/producers/SingleProducer.java @@ -64,8 +64,7 @@ public void request(long n) { try { c.onNext(v); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - c.onError(OnErrorThrowable.addValueAsLastCause(e, v)); + Exceptions.throwOrReport(e, c, v); return; } // eagerly check for unsubscription diff --git a/src/test/java/rx/internal/operators/OperatorFilterTest.java b/src/test/java/rx/internal/operators/OperatorFilterTest.java index aaa9484be0..f1f086666c 100644 --- a/src/test/java/rx/internal/operators/OperatorFilterTest.java +++ b/src/test/java/rx/internal/operators/OperatorFilterTest.java @@ -16,18 +16,16 @@ package rx.internal.operators; 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 static org.mockito.Mockito.*; import java.util.concurrent.CountDownLatch; -import org.junit.Test; +import org.junit.*; import org.mockito.Mockito; -import rx.Observable; -import rx.Observer; -import rx.functions.Func1; +import rx.*; +import rx.exceptions.*; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; @@ -144,4 +142,29 @@ public void onNext(Integer t) { // this will wait forever unless OperatorTake handles the request(n) on filtered items latch.await(); } + + @Test + public void testFatalError() { + try { + Observable.just(1) + .filter(new Func1() { + @Override + public Boolean call(Integer t) { + return true; + } + }) + .first() + .subscribe(new Action1() { + @Override + public void call(Integer t) { + throw new TestException(); + } + }); + Assert.fail("No exception was thrown"); + } catch (OnErrorNotImplementedException ex) { + if (!(ex.getCause() instanceof TestException)) { + Assert.fail("Failed to report the original exception, instead: " + ex.getCause()); + } + } + } } From 0c8b250ea1d0c9e72a432763f4540e3c81c238b0 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Fri, 14 Aug 2015 03:25:14 +0300 Subject: [PATCH 375/857] Add Observable.fromCallable() as a companion for Observable.defer() --- src/main/java/rx/Observable.java | 23 +++ .../operators/OnSubscribeFromCallable.java | 38 +++++ .../OnSubscribeFromCallableTest.java | 140 ++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 src/main/java/rx/internal/operators/OnSubscribeFromCallable.java create mode 100644 src/test/java/rx/internal/operators/OnSubscribeFromCallableTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 79825d622b..5d6d77c15f 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1250,6 +1250,29 @@ public final static Observable from(T[] array) { return from(Arrays.asList(array)); } + /** + * Returns an Observable that invokes passed function and emits its result for each new Observer that subscribes. + *

+ * Allows you to defer execution of passed function until Observer subscribes to the Observable. + * It makes passed function "lazy". + * Result of the function invocation will be emitted by the Observable. + *

+ *
Scheduler:
+ *
{@code fromCallable} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param func + * function which execution should be deferred, it will be invoked when Observer will subscribe to the Observable + * @param + * the type of the item emitted by the Observable + * @return an Observable whose {@link Observer}s' subscriptions trigger an invocation of the given function + * @see #defer(Func0) + */ + @Experimental + public static Observable fromCallable(Callable func) { + return create(new OnSubscribeFromCallable(func)); + } + /** * Returns an Observable that emits a sequential number every specified interval of time. *

diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromCallable.java b/src/main/java/rx/internal/operators/OnSubscribeFromCallable.java new file mode 100644 index 0000000000..35eb62f04e --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeFromCallable.java @@ -0,0 +1,38 @@ +package rx.internal.operators; + +import rx.Observable; +import rx.Subscriber; +import rx.exceptions.Exceptions; +import rx.internal.producers.SingleDelayedProducer; + +import java.util.concurrent.Callable; + +/** + * Do not invoke the function until an Observer subscribes; Invokes function on each + * subscription. + *

+ * Pass {@code fromCallable} a function, and {@code fromCallable} will call this function to emit result of invocation + * afresh each time a new Observer subscribes. + */ +public final class OnSubscribeFromCallable implements Observable.OnSubscribe { + + private final Callable resultFactory; + + public OnSubscribeFromCallable(Callable resultFactory) { + this.resultFactory = resultFactory; + } + + @Override + public void call(Subscriber subscriber) { + final SingleDelayedProducer singleDelayedProducer = new SingleDelayedProducer(subscriber); + + subscriber.setProducer(singleDelayedProducer); + + try { + singleDelayedProducer.setValue(resultFactory.call()); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + subscriber.onError(t); + } + } +} diff --git a/src/test/java/rx/internal/operators/OnSubscribeFromCallableTest.java b/src/test/java/rx/internal/operators/OnSubscribeFromCallableTest.java new file mode 100644 index 0000000000..a4da6e3208 --- /dev/null +++ b/src/test/java/rx/internal/operators/OnSubscribeFromCallableTest.java @@ -0,0 +1,140 @@ +package rx.internal.operators; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import rx.Observable; +import rx.Observer; +import rx.Subscription; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; + +import static org.mockito.Mockito.*; +import static rx.schedulers.Schedulers.computation; + +public class OnSubscribeFromCallableTest { + + @SuppressWarnings("unchecked") + @Test + public void shouldNotInvokeFuncUntilSubscription() throws Exception { + Callable func = mock(Callable.class); + + when(func.call()).thenReturn(new Object()); + + Observable fromCallableObservable = Observable.fromCallable(func); + + verifyZeroInteractions(func); + + fromCallableObservable.subscribe(); + + verify(func).call(); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnNextAndOnCompleted() throws Exception { + Callable func = mock(Callable.class); + + when(func.call()).thenReturn("test_value"); + + Observable fromCallableObservable = Observable.fromCallable(func); + + Observer observer = mock(Observer.class); + + fromCallableObservable.subscribe(observer); + + verify(observer).onNext("test_value"); + verify(observer).onCompleted(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnError() throws Exception { + Callable func = mock(Callable.class); + + Throwable throwable = new IllegalStateException("Test exception"); + when(func.call()).thenThrow(throwable); + + Observable fromCallableObservable = Observable.fromCallable(func); + + Observer observer = mock(Observer.class); + + fromCallableObservable.subscribe(observer); + + verify(observer, never()).onNext(anyObject()); + verify(observer, never()).onCompleted(); + verify(observer).onError(throwable); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Exception { + Callable func = mock(Callable.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.call()).thenAnswer(new Answer() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Observable fromCallableObservable = Observable.fromCallable(func); + + Observer observer = mock(Observer.class); + + Subscription subscription = fromCallableObservable + .subscribeOn(computation()) + .subscribe(observer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + subscription.unsubscribe(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).call(); + + // Observer must not be notified at all + verifyZeroInteractions(observer); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAllowToThrowCheckedException() { + final Exception checkedException = new Exception("test exception"); + + Observable fromCallableObservable = Observable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + throw checkedException; + } + }); + + Observer observer = mock(Observer.class); + + fromCallableObservable.subscribe(observer); + + verify(observer).onError(checkedException); + verifyNoMoreInteractions(observer); + } +} \ No newline at end of file From 5d27630463bec62781fd63dd5e3e6cd41eb96f17 Mon Sep 17 00:00:00 2001 From: wrightm Date: Thu, 27 Aug 2015 22:05:22 +0100 Subject: [PATCH 376/857] Fix to Notification equals method. --- src/main/java/rx/Notification.java | 5 ++ src/test/java/rx/NotificationTest.java | 64 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/test/java/rx/NotificationTest.java diff --git a/src/main/java/rx/Notification.java b/src/main/java/rx/Notification.java index 17a23d1031..b708b58766 100644 --- a/src/main/java/rx/Notification.java +++ b/src/main/java/rx/Notification.java @@ -202,6 +202,11 @@ public boolean equals(Object obj) { return false; if (hasThrowable() && !getThrowable().equals(notification.getThrowable())) return false; + if(!hasValue() && !hasThrowable() && notification.hasValue()) + return false; + if(!hasValue() && !hasThrowable() && notification.hasThrowable()) + return false; + return true; } } diff --git a/src/test/java/rx/NotificationTest.java b/src/test/java/rx/NotificationTest.java new file mode 100644 index 0000000000..cf33fb991a --- /dev/null +++ b/src/test/java/rx/NotificationTest.java @@ -0,0 +1,64 @@ +package rx; + +import org.junit.Assert; +import org.junit.Test; + +public class NotificationTest { + + @Test + public void testOnNextIntegerNotificationDoesNotEqualNullNotification(){ + final Notification integerNotification = Notification.createOnNext(1); + final Notification nullNotification = Notification.createOnNext(null); + Assert.assertFalse(integerNotification.equals(nullNotification)); + } + + @Test + public void testOnNextNullNotificationDoesNotEqualIntegerNotification(){ + final Notification integerNotification = Notification.createOnNext(1); + final Notification nullNotification = Notification.createOnNext(null); + Assert.assertFalse(nullNotification.equals(integerNotification)); + } + + @Test + public void testOnNextIntegerNotificationsWhenEqual(){ + final Notification integerNotification = Notification.createOnNext(1); + final Notification integerNotification2 = Notification.createOnNext(1); + Assert.assertTrue(integerNotification.equals(integerNotification2)); + } + + @Test + public void testOnNextIntegerNotificationsWhenNotEqual(){ + final Notification integerNotification = Notification.createOnNext(1); + final Notification integerNotification2 = Notification.createOnNext(2); + Assert.assertFalse(integerNotification.equals(integerNotification2)); + } + + @Test + public void testOnErrorIntegerNotificationDoesNotEqualNullNotification(){ + final Notification integerNotification = Notification.createOnError(new Exception()); + final Notification nullNotification = Notification.createOnError(null); + Assert.assertFalse(integerNotification.equals(nullNotification)); + } + + @Test + public void testOnErrorNullNotificationDoesNotEqualIntegerNotification(){ + final Notification integerNotification = Notification.createOnError(new Exception()); + final Notification nullNotification = Notification.createOnError(null); + Assert.assertFalse(nullNotification.equals(integerNotification)); + } + + @Test + public void testOnErrorIntegerNotificationsWhenEqual(){ + final Exception exception = new Exception(); + final Notification onErrorNotification = Notification.createOnError(exception); + final Notification onErrorNotification2 = Notification.createOnError(exception); + Assert.assertTrue(onErrorNotification.equals(onErrorNotification2)); + } + + @Test + public void testOnErrorIntegerNotificationWhenNotEqual(){ + final Notification onErrorNotification = Notification.createOnError(new Exception()); + final Notification onErrorNotification2 = Notification.createOnError(new Exception()); + Assert.assertFalse(onErrorNotification.equals(onErrorNotification2)); + } +} From ba7f9103f2706c162f0a27d4ca4be35acf3e78fa Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Tue, 28 Jul 2015 17:55:34 -0700 Subject: [PATCH 377/857] Implementing the SyncOnSubscribe --- .../java/rx/observables/SyncOnSubscribe.java | 465 +++++++++ .../rx/jmh/InputWithIncrementingInteger.java | 22 +- .../observables/BlockingObservablePerf.java | 32 +- src/perf/java/rx/observables/MultiInput.java | 36 + src/perf/java/rx/observables/SingleInput.java | 36 + .../rx/observables/SyncOnSubscribePerf.java | 118 +++ .../rx/observables/SyncOnSubscribeTest.java | 982 ++++++++++++++++++ 7 files changed, 1649 insertions(+), 42 deletions(-) create mode 100644 src/main/java/rx/observables/SyncOnSubscribe.java create mode 100644 src/perf/java/rx/observables/MultiInput.java create mode 100644 src/perf/java/rx/observables/SingleInput.java create mode 100644 src/perf/java/rx/observables/SyncOnSubscribePerf.java create mode 100644 src/test/java/rx/observables/SyncOnSubscribeTest.java diff --git a/src/main/java/rx/observables/SyncOnSubscribe.java b/src/main/java/rx/observables/SyncOnSubscribe.java new file mode 100644 index 0000000000..47a6c34024 --- /dev/null +++ b/src/main/java/rx/observables/SyncOnSubscribe.java @@ -0,0 +1,465 @@ +/** + * 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.observables; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; + +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Producer; +import rx.Subscriber; +import rx.Subscription; +import rx.annotations.Experimental; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Func0; +import rx.functions.Func2; +import rx.internal.operators.BackpressureUtils; +import rx.plugins.RxJavaPlugins; + +/** + * A utility class to create {@code OnSubscribe} functions that respond correctly to back + * pressure requests from subscribers. This is an improvement over + * {@link rx.Observable#create(OnSubscribe) Observable.create(OnSubscribe)} which does not provide + * any means of managing back pressure requests out-of-the-box. + * + * @param + * the type of the user-define state used in {@link #generateState() generateState(S)} , + * {@link #next(Object, Subscriber) next(S, Subscriber)}, and + * {@link #onUnsubscribe(Object) onUnsubscribe(S)}. + * @param + * the type of {@code Subscribers} that will be compatible with {@code this}. + */ +@Experimental +public abstract class SyncOnSubscribe implements OnSubscribe { + + /* (non-Javadoc) + * @see rx.functions.Action1#call(java.lang.Object) + */ + @Override + public final void call(final Subscriber subscriber) { + S state = generateState(); + SubscriptionProducer p = new SubscriptionProducer(subscriber, this, state); + subscriber.add(p); + subscriber.setProducer(p); + } + + /** + * Executed once when subscribed to by a subscriber (via {@link OnSubscribe#call(Subscriber)}) + * to produce a state value. This value is passed into {@link #next(Object, Observer) next(S + * state, Observer observer)} on the first iteration. Subsequent iterations of {@code next} + * will receive the state returned by the previous invocation of {@code next}. + * + * @return the initial state value + */ + protected abstract S generateState(); + + /** + * Called to produce data to the downstream subscribers. To emit data to a downstream subscriber + * call {@code observer.onNext(t)}. To signal an error condition call + * {@code observer.onError(throwable)} or throw an Exception. To signal the end of a data stream + * call {@code + * observer.onCompleted()}. Implementations of this method must follow the following rules. + * + *
    + *
  • Must not call {@code observer.onNext(t)} more than 1 time per invocation.
  • + *
  • Must not call {@code observer.onNext(t)} concurrently.
  • + *
+ * + * The value returned from an invocation of this method will be passed in as the {@code state} + * argument of the next invocation of this method. + * + * @param state + * the state value (from {@link #generateState()} on the first invocation or the + * previous invocation of this method. + * @param observer + * the observer of data emitted by + * @return the next iteration's state value + */ + protected abstract S next(S state, Observer observer); + + /** + * Clean up behavior that is executed after the downstream subscriber's subscription is + * unsubscribed. This method will be invoked exactly once. + * + * @param state + * the last state value prior from {@link #generateState()} or + * {@link #next(Object, Observer) next(S, Observer<T>)} before unsubscribe. + */ + protected void onUnsubscribe(S state) { + + } + + /** + * Generates a synchronous {@link SyncOnSubscribe} that calls the provided {@code next} function + * to generate data to downstream subscribers. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @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. + */ + @Experimental + public static OnSubscribe createSingleState(Func0 generator, + final Action2> next) { + Func2, S> nextFunc = new Func2, S>() { + @Override + public S call(S state, Observer subscriber) { + next.call(state, subscriber); + return state; + } + }; + return new SyncOnSubscribeImpl(generator, nextFunc); + } + + /** + * Generates a synchronous {@link SyncOnSubscribe} that calls the provided {@code next} function + * to generate data to downstream subscribers. + * + * This overload creates a SyncOnSubscribe without an explicit clean up step. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @param next + * produces data to the downstream subscriber (see {@link #next(Object, 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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createSingleState(Func0 generator, + final Action2> next, + final Action1 onUnsubscribe) { + Func2, S> nextFunc = new Func2, S>() { + @Override + public S call(S state, Observer subscriber) { + next.call(state, subscriber); + return state; + } + }; + return new SyncOnSubscribeImpl(generator, nextFunc, onUnsubscribe); + } + + /** + * Generates a synchronous {@link SyncOnSubscribe} that calls the provided {@code next} function + * to generate data to downstream subscribers. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @param next + * produces data to the downstream subscriber (see {@link #next(Object, 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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateful(Func0 generator, + Func2, ? extends S> next, + Action1 onUnsubscribe) { + return new SyncOnSubscribeImpl(generator, next, onUnsubscribe); + } + + /** + * Generates a synchronous {@link SyncOnSubscribe} that calls the provided {@code next} function + * to generate data to downstream subscribers. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateful(Func0 generator, + Func2, ? extends S> next) { + return new SyncOnSubscribeImpl(generator, next); + } + + /** + * Generates a synchronous {@link SyncOnSubscribe} that calls the provided {@code next} function + * to generate data to downstream subscribers. + * + * This overload creates a "state-less" SyncOnSubscribe which does not have an explicit state + * value. This should be used when the {@code next} function closes over it's state. + * + * @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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateless(final Action1> next) { + Func2, Void> nextFunc = new Func2, Void>() { + @Override + public Void call(Void state, Observer subscriber) { + next.call(subscriber); + return state; + } + }; + return new SyncOnSubscribeImpl(nextFunc); + } + + /** + * Generates a synchronous {@link SyncOnSubscribe} that calls the provided {@code next} function + * to generate data to downstream subscribers. + * + * This overload creates a "state-less" SyncOnSubscribe which does not have an explicit state + * value. This should be used when the {@code next} function closes over it's state. + * + * @param next + * produces data to the downstream subscriber (see {@link #next(Object, 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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateless(final Action1> next, + final Action0 onUnsubscribe) { + Func2, Void> nextFunc = new Func2, Void>() { + @Override + public Void call(Void state, Observer subscriber) { + next.call(subscriber); + return null; + } + }; + Action1 wrappedOnUnsubscribe = new Action1(){ + @Override + public void call(Void t) { + onUnsubscribe.call(); + }}; + return new SyncOnSubscribeImpl(nextFunc, wrappedOnUnsubscribe); + } + + /** + * An implementation of SyncOnSubscribe that delegates + * {@link SyncOnSubscribe#next(Object, Subscriber)}, {@link SyncOnSubscribe#generateState()}, + * and {@link SyncOnSubscribe#onUnsubscribe(Object)} to provided functions/closures. + * + * @param + * the type of the user-defined state + * @param + * the type of compatible Subscribers + */ + private static final class SyncOnSubscribeImpl extends SyncOnSubscribe { + private final Func0 generator; + private final Func2, ? extends S> next; + private final Action1 onUnsubscribe; + + private SyncOnSubscribeImpl(Func0 generator, Func2, ? extends S> next, Action1 onUnsubscribe) { + this.generator = generator; + this.next = next; + this.onUnsubscribe = onUnsubscribe; + } + + public SyncOnSubscribeImpl(Func0 generator, Func2, ? extends S> next) { + this(generator, next, null); + } + + public SyncOnSubscribeImpl(Func2, S> next, Action1 onUnsubscribe) { + this(null, next, onUnsubscribe); + } + + public SyncOnSubscribeImpl(Func2, S> nextFunc) { + this(null, nextFunc, null); + } + + @Override + protected S generateState() { + return generator == null ? null : generator.call(); + } + + @Override + protected S next(S state, Observer observer) { + return next.call(state, observer); + } + + @Override + protected void onUnsubscribe(S state) { + if (onUnsubscribe != null) + onUnsubscribe.call(state); + } + } + + /** + * Contains the producer loop that reacts to downstream requests of work. + * + * @param + * the type of compatible Subscribers + */ + private static class SubscriptionProducer + extends AtomicLong implements Producer, Subscription, Observer { + /** */ + private static final long serialVersionUID = -3736864024352728072L; + private final Subscriber actualSubscriber; + private final SyncOnSubscribe parent; + private boolean onNextCalled; + private boolean hasTerminated; + + private S state; + + volatile int isUnsubscribed; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater IS_UNSUBSCRIBED = + AtomicIntegerFieldUpdater.newUpdater(SubscriptionProducer.class, "isUnsubscribed"); + + private SubscriptionProducer(final Subscriber subscriber, SyncOnSubscribe parent, S state) { + this.actualSubscriber = subscriber; + this.parent = parent; + this.state = state; + } + + @Override + public boolean isUnsubscribed() { + return isUnsubscribed != 0; + } + + @Override + public void unsubscribe() { + IS_UNSUBSCRIBED.compareAndSet(this, 0, 1); + if (get() == 0L) + parent.onUnsubscribe(state); + } + + @Override + public void request(long n) { + if (n > 0 && BackpressureUtils.getAndAddRequest(this, n) == 0L) { + if (n == Long.MAX_VALUE) { + fastpath(); + } else { + slowPath(n); + } + } + } + + void fastpath() { + final SyncOnSubscribe p = parent; + Subscriber a = actualSubscriber; + + if (isUnsubscribed()) { + p.onUnsubscribe(state); + return; + } + + for (;;) { + try { + onNextCalled = false; + nextIteration(p); + } catch (Throwable ex) { + handleThrownError(p, a, state, ex); + return; + } + if (hasTerminated || isUnsubscribed()) { + p.onUnsubscribe(state); + return; + } + } + } + + private void handleThrownError(final SyncOnSubscribe p, Subscriber a, S st, Throwable ex) { + if (hasTerminated) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); + } else { + hasTerminated = true; + a.onError(ex); + p.onUnsubscribe(st); + } + } + + void slowPath(long n) { + final SyncOnSubscribe p = parent; + Subscriber a = actualSubscriber; + long numRequested = n; + for (;;) { + if (isUnsubscribed()) { + p.onUnsubscribe(state); + return; + } + long numRemaining = numRequested; + do { + try { + onNextCalled = false; + nextIteration(p); + } catch (Throwable ex) { + handleThrownError(p, a, state, ex); + return; + } + if (hasTerminated || isUnsubscribed()) { + p.onUnsubscribe(state); + return; + } + if (onNextCalled) + numRemaining--; + } while (numRemaining != 0L); + + numRequested = addAndGet(-numRequested); + if (numRequested == 0L) { + break; + } + } + } + + private void nextIteration(final SyncOnSubscribe parent) { + state = parent.next(state, this); + } + + @Override + public void onCompleted() { + if (hasTerminated) { + throw new IllegalStateException("Terminal event already emitted."); + } + hasTerminated = true; + if (!actualSubscriber.isUnsubscribed()) { + actualSubscriber.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + if (hasTerminated) { + throw new IllegalStateException("Terminal event already emitted."); + } + hasTerminated = true; + if (!actualSubscriber.isUnsubscribed()) { + actualSubscriber.onError(e); + } + } + + @Override + public void onNext(T value) { + if (onNextCalled) { + throw new IllegalStateException("onNext called multiple times!"); + } + onNextCalled = true; + actualSubscriber.onNext(value); + } + } + + +} \ No newline at end of file diff --git a/src/perf/java/rx/jmh/InputWithIncrementingInteger.java b/src/perf/java/rx/jmh/InputWithIncrementingInteger.java index f86bc28117..6760202024 100644 --- a/src/perf/java/rx/jmh/InputWithIncrementingInteger.java +++ b/src/perf/java/rx/jmh/InputWithIncrementingInteger.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package rx.jmh; import java.util.Iterator; @@ -40,46 +41,43 @@ public abstract class InputWithIncrementingInteger { @Setup public void setup(final Blackhole bh) { this.bh = bh; - observable = Observable.range(0, getSize()); + final int size = getSize(); + observable = Observable.range(0, size); firehose = Observable.create(new OnSubscribe() { @Override public void call(Subscriber s) { - for (int i = 0; i < getSize(); i++) { + for (int i = 0; i < size; i++) { s.onNext(i); } s.onCompleted(); } }); - iterable = new Iterable() { - @Override public Iterator iterator() { return new Iterator() { - int i = 0; - + @Override public boolean hasNext() { - return i < getSize(); + return i < size; } - + @Override public Integer next() { + Blackhole.consumeCPU(10); return i++; } - + @Override public void remove() { - + } - }; } - }; observer = new Observer() { diff --git a/src/perf/java/rx/observables/BlockingObservablePerf.java b/src/perf/java/rx/observables/BlockingObservablePerf.java index 4cb18d31c0..7c6b00029e 100644 --- a/src/perf/java/rx/observables/BlockingObservablePerf.java +++ b/src/perf/java/rx/observables/BlockingObservablePerf.java @@ -15,48 +15,20 @@ */ package rx.observables; +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.State; -import rx.jmh.InputWithIncrementingInteger; - -import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Thread) public class BlockingObservablePerf { - @State(Scope.Thread) - public static class MultiInput extends InputWithIncrementingInteger { - - @Param({ "1", "1000", "1000000" }) - public int size; - - @Override - public int getSize() { - return size; - } - - } - - @State(Scope.Thread) - public static class SingleInput extends InputWithIncrementingInteger { - - @Param({ "1" }) - public int size; - - @Override - public int getSize() { - return size; - } - - } - @Benchmark public int benchSingle(final SingleInput input) { return input.observable.toBlocking().single(); diff --git a/src/perf/java/rx/observables/MultiInput.java b/src/perf/java/rx/observables/MultiInput.java new file mode 100644 index 0000000000..e607249d07 --- /dev/null +++ b/src/perf/java/rx/observables/MultiInput.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.observables; + +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +import rx.jmh.InputWithIncrementingInteger; + +@State(Scope.Thread) +public class MultiInput extends InputWithIncrementingInteger { + + @Param({ "1", "1000", "1000000" }) + public int size; + + @Override + public int getSize() { + return size; + } + +} diff --git a/src/perf/java/rx/observables/SingleInput.java b/src/perf/java/rx/observables/SingleInput.java new file mode 100644 index 0000000000..7949efcfa5 --- /dev/null +++ b/src/perf/java/rx/observables/SingleInput.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.observables; + +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +import rx.jmh.InputWithIncrementingInteger; + +@State(Scope.Thread) +public class SingleInput extends InputWithIncrementingInteger { + + @Param({ "1" }) + public int size; + + @Override + public int getSize() { + return size; + } + +} diff --git a/src/perf/java/rx/observables/SyncOnSubscribePerf.java b/src/perf/java/rx/observables/SyncOnSubscribePerf.java new file mode 100644 index 0000000000..8417bf3a8e --- /dev/null +++ b/src/perf/java/rx/observables/SyncOnSubscribePerf.java @@ -0,0 +1,118 @@ +/** + * 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.observables; + +import java.util.Iterator; +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.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.internal.operators.OnSubscribeFromIterable; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class SyncOnSubscribePerf { + + public static void main(String[] args) { + SingleInput singleInput = new SingleInput(); + singleInput.size = 1; + singleInput.setup(generated._jmh_tryInit_()); + SyncOnSubscribePerf perf = new SyncOnSubscribePerf(); + perf.benchSyncOnSubscribe(singleInput); + } + private static class generated { + private static Blackhole _jmh_tryInit_() { + return new Blackhole(); + } + } + + private static OnSubscribe createSyncOnSubscribe(final Iterator iterator) { + return new SyncOnSubscribe(){ + + @Override + protected Void generateState() { + return null; + } + + @Override + protected Void next(Void state, Observer observer) { + if (iterator.hasNext()) { + observer.onNext(iterator.next()); + } + else + observer.onCompleted(); + return null; + } + }; + } + +// @Benchmark +// @Group("single") + public void benchSyncOnSubscribe(final SingleInput input) { + createSyncOnSubscribe(input.iterable.iterator()).call(input.newSubscriber()); + } + +// @Benchmark +// @Group("single") + public void benchFromIterable(final SingleInput input) { + new OnSubscribeFromIterable(input.iterable).call(input.newSubscriber()); + } + +// @Benchmark +// @Group("single") + public void benchAbstractOnSubscribe(final SingleInput input) { + final Iterator iterator = input.iterable.iterator(); + createAbstractOnSubscribe(iterator).call(input.newSubscriber()); + } + + private AbstractOnSubscribe createAbstractOnSubscribe(final Iterator iterator) { + return new AbstractOnSubscribe() { + @Override + protected void next(rx.observables.AbstractOnSubscribe.SubscriptionState state) { + if (iterator.hasNext()) + state.onNext(iterator.next()); + else + state.onCompleted(); + }}; + } + + @Benchmark +// @Group("multi") + public void benchSyncOnSubscribe2(final MultiInput input) { + createSyncOnSubscribe(input.iterable.iterator()).call(input.newSubscriber()); + } + +// @Benchmark +// @Group("multi") + public void benchAbstractOnSubscribe2(final MultiInput input) { + createAbstractOnSubscribe(input.iterable.iterator()).call(input.newSubscriber()); + } + + @Benchmark +// @Group("multi") + public void benchFromIterable2(final MultiInput input) { + new OnSubscribeFromIterable(input.iterable).call(input.newSubscriber()); + } +} diff --git a/src/test/java/rx/observables/SyncOnSubscribeTest.java b/src/test/java/rx/observables/SyncOnSubscribeTest.java new file mode 100644 index 0000000000..91421d502a --- /dev/null +++ b/src/test/java/rx/observables/SyncOnSubscribeTest.java @@ -0,0 +1,982 @@ +/** + * 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.observables; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.isA; +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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Matchers; +import org.mockito.Mockito; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observable.Operator; +import rx.Observer; +import rx.Producer; +import rx.Subscriber; +import rx.exceptions.TestException; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Func0; +import rx.functions.Func2; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.schedulers.TestScheduler; + +/** + * Test if SyncOnSubscribe adheres to the usual unsubscription and backpressure contracts. + */ +public class SyncOnSubscribeTest { + @Test + public void testObservableJustEquivalent() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onNext(1); + subscriber.onCompleted(); + }}); + + TestSubscriber ts = new TestSubscriber(); + + Observable.create(os).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertReceivedOnNext(Arrays.asList(1)); + } + + @Test + public void testStateAfterTerminal() { + final AtomicInteger finalStateValue = new AtomicInteger(-1); + OnSubscribe os = SyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func2, Integer>() { + @Override + public Integer call(Integer state, Observer subscriber) { + subscriber.onNext(state); + subscriber.onCompleted(); + return state + 1; + }}, new Action1() { + @Override + public void call(Integer t) { + finalStateValue.set(t); + }}); + + TestSubscriber ts = new TestSubscriber(); + + Observable.create(os).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminalEvent(); + ts.assertValue(1); + assertEquals(2, finalStateValue.get()); + } + + @Test + public void testMultipleOnNextValuesCallsOnError() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onCompleted(); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, times(1)).onNext(1); + verify(o, never()).onNext(2); + verify(o, never()).onCompleted(); + verify(o, times(1)).onError(any(IllegalStateException.class)); + } + + @Test + public void testMultipleOnCompleted() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onNext(1); + subscriber.onCompleted(); + subscriber.onCompleted(); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, times(1)).onNext(1); + verify(o, times(1)).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void testOnNextAfterOnComplete() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onNext(1); + subscriber.onCompleted(); + subscriber.onNext(1); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, times(1)).onNext(1); + verify(o, times(1)).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + + @SuppressWarnings("serial") + private static class FooException extends RuntimeException { + public FooException(String string) { + super(string); + } + } + + @Test + public void testMultipleOnErrors() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onNext(1); + subscriber.onError(new TestException("Forced failure 1")); + subscriber.onError(new FooException("Should not see this error.")); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, times(1)).onNext(1); + verify(o, never()).onCompleted(); + verify(o, times(1)).onError(isA(TestException.class)); + verify(o, never()).onError(isA(FooException.class)); + } + + @Test + public void testEmpty() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onCompleted(); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onCompleted(); + } + + @Test + public void testNever() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + + }}); + + + Observable neverObservable = Observable.create(os).subscribeOn(Schedulers.newThread()); + Observable merged = Observable.amb(neverObservable, Observable.timer(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread())); + Iterator values = merged.toBlocking().toIterable().iterator(); + + assertTrue((values.hasNext())); + assertEquals(0l, values.next()); + } + + @Test + public void testThrows() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + throw new TestException("Forced failure"); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onCompleted(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void testThrowAfterCompleteFastPath() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onCompleted(); + throw new TestException("Forced failure"); + }}); + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, times(1)).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void testThrowsSlowPath() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + throw new TestException("Forced failure"); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + TestSubscriber ts = new TestSubscriber(o) { + @Override + public void onStart() { + requestMore(0); // don't start right away + } + }; + + Observable.create(os).subscribe(ts); + + ts.requestMore(1); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onCompleted(); + verify(o, times(1)).onError(any(TestException.class)); + } + + @Test + public void testError() { + OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { + @Override + public void call(Observer subscriber) { + subscriber.onError(new TestException("Forced failure")); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + Observable.create(os).subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o).onError(any(TestException.class)); + verify(o, never()).onCompleted(); + } + + @Test + public void testRange() { + final int start = 1; + final int count = 4000; + OnSubscribe os = SyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return start; + }}, + new Func2, Integer>() { + @Override + public Integer call(Integer state, Observer subscriber) { + subscriber.onNext(state); + if (state == count) { + subscriber.onCompleted(); + } + return state + 1; + } + }); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + Observable.create(os).subscribe(o); + + verify(o, never()).onError(any(TestException.class)); + inOrder.verify(o, times(count)).onNext(any(Integer.class)); + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testFromIterable() { + int n = 400; + final List source = new ArrayList(); + for (int i = 0; i < n; i++) { + source.add(i); + } + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0>() { + @Override + public Iterator call() { + return source.iterator(); + }}, + new Func2, Observer, Iterator>() { + @Override + public Iterator call(Iterator it, Observer observer) { + if (it.hasNext()) { + observer.onNext(it.next()); + } + if (!it.hasNext()) { + observer.onCompleted(); + } + return it; + }}); + + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + Observable.create(os).subscribe(o); + + verify(o, never()).onError(any(TestException.class)); + inOrder.verify(o, times(n)).onNext(any()); + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testInfiniteTake() { + final int start = 0; + final int finalCount = 4000; + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0() { + @Override + public Integer call() { + return start; + }}, + new Func2, Integer>() { + @Override + public Integer call(Integer state, Observer observer) { + observer.onNext(state); + return state + 1; + }}); + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + Observable.create(os).take(finalCount).subscribe(o); + + verify(o, never()).onError(any(Throwable.class)); + inOrder.verify(o, times(finalCount)).onNext(any()); + inOrder.verify(o).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testInfiniteRequestSome() { + final int finalCount = 4000; + final int start = 0; + + @SuppressWarnings("unchecked") + Action1 onUnSubscribe = mock(Action1.class); + + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0() { + @Override + public Integer call() { + return start; + }}, + new Func2, Integer>() { + @Override + public Integer call(Integer state, Observer observer) { + observer.onNext(state); + return state + 1; + }}, + onUnSubscribe); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + TestSubscriber ts = new TestSubscriber(o) { + @Override + public void onStart() { + requestMore(0); // don't start right away + } + }; + + Observable.create(os).subscribe(ts); + + ts.requestMore(finalCount); + + verify(o, never()).onError(any(Throwable.class)); + verify(o, never()).onCompleted(); + inOrder.verify(o, times(finalCount)).onNext(any()); + inOrder.verifyNoMoreInteractions(); + // unsubscribe does not take place because subscriber is still in process of requesting + verify(onUnSubscribe, never()).call(any(Integer.class)); + } + + @Test + public void testUnsubscribeDownstream() { + @SuppressWarnings("unchecked") + Action1 onUnSubscribe = mock(Action1.class); + + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0() { + @Override + public Integer call() { + return null; + }}, + new Func2, Integer>() { + @Override + public Integer call(Integer state, Observer observer) { + observer.onNext(state); + return state; + }}, + onUnSubscribe); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + TestSubscriber ts = new TestSubscriber(o); + + Observable.create(os).lift(new Operator(){ + @Override + public Subscriber call(final Subscriber subscriber) { + return new Subscriber(){ + @Override + public void setProducer(Producer p) { + p.request(Long.MAX_VALUE); + } + + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(Object t) { + subscriber.onNext(t); + unsubscribe(); + }}; + }}).take(1).subscribe(ts); + + verify(o, never()).onError(any(Throwable.class)); + verify(onUnSubscribe, times(1)).call(any(Integer.class)); + } + + @Test + public void testConcurrentRequests() throws InterruptedException { + final int count1 = 1000; + final int count2 = 1000; + final int finalCount = count1 + count2; + final int start = 1; + final CountDownLatch l1 = new CountDownLatch(1); + final CountDownLatch l2 = new CountDownLatch(1); + + @SuppressWarnings("unchecked") + Action1 onUnSubscribe = mock(Action1.class); + + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0() { + @Override + public Integer call() { + return start; + }}, + new Func2, Integer>() { + @Override + public Integer call(Integer state, Observer observer) { + // countdown so the other thread is certain to make a concurrent request + l2.countDown(); + // wait until the 2nd request returns then proceed + try { + if (!l1.await(1, TimeUnit.SECONDS)) + throw new IllegalStateException(); + } catch (InterruptedException e) {} + observer.onNext(state); + if (state == finalCount) + observer.onCompleted(); + return state + 1; + }}, + onUnSubscribe); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + final TestSubscriber ts = new TestSubscriber(o); + Observable.create(os).subscribeOn(Schedulers.newThread()).subscribe(ts); + + // wait until the first request has started processing + try { + if (!l2.await(1, TimeUnit.SECONDS)) + throw new IllegalStateException(); + } catch (InterruptedException e) {} + // make a concurrent request, this should return + ts.requestMore(count2); + // unblock the 1st thread to proceed fulfilling requests + l1.countDown(); + + ts.awaitTerminalEvent(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + + inOrder.verify(o, times(finalCount)).onNext(any()); + inOrder.verify(o, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + verify(onUnSubscribe, times(1)).call(any(Integer.class)); + } + + @Test + public void testUnsubscribeOutsideOfLoop() { + final AtomicInteger calledUnsubscribe = new AtomicInteger(0); + final AtomicBoolean currentlyEvaluating = new AtomicBoolean(false); + + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0() { + @Override + public Void call() { + return null; + }}, + new Func2, Void>() { + @Override + public Void call(Void state, Observer observer) { + currentlyEvaluating.set(true); + observer.onNext(null); + currentlyEvaluating.set(false); + return null; + }}, + new Action1(){ + @Override + public void call(Void t) { + calledUnsubscribe.incrementAndGet(); + assertFalse(currentlyEvaluating.get()); + }}); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + final TestSubscriber ts = new TestSubscriber(o) { + @Override + public void onStart() { + requestMore(1); + } + }; + Observable.create(os).lift(new Operator(){ + @Override + public Subscriber call(final Subscriber subscriber) { + return new Subscriber(){ + @Override + public void setProducer(Producer p) { + p.request(1); + } + @Override + public void onCompleted() { + subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(final Void t) { + new Thread(new Runnable(){ + @Override + public void run() { + subscriber.onNext(t); + unsubscribe(); + subscriber.onCompleted(); + }}).start(); + }}; + }}).subscribe(ts); + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertUnsubscribed(); + assertEquals(1, calledUnsubscribe.get()); + } + + @Test + public void testIndependentStates() { + int count = 100; + final ConcurrentHashMap subscribers = new ConcurrentHashMap(); + + @SuppressWarnings("unchecked") + Action1> onUnSubscribe = mock(Action1.class); + + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0>() { + @Override + public Map call() { + return subscribers; + }}, + new Func2, Observer, Map>() { + @Override + public Map call(Map state, Observer observer) { + state.put(observer, observer); + observer.onCompleted(); + return state; + }}, + onUnSubscribe); + + Observable source = Observable.create(os); + for (int i = 0; i < count; i++) { + source.subscribe(); + } + + assertEquals(count, subscribers.size()); + verify(onUnSubscribe, times(count)).call(Matchers.>any()); + } + + @Test(timeout = 3000) + public void testSubscribeOn() { + final int start = 1; + final int count = 400; + final AtomicInteger countUnsubscribe = new AtomicInteger(0); + final int numSubscribers = 4; + + OnSubscribe os = SyncOnSubscribe.createStateful( + new Func0() { + @Override + public Integer call() { + return start; + }}, + new Func2, Integer>() { + @Override + public Integer call(Integer calls, Observer observer) { + if (calls > count) { + observer.onCompleted(); + } else { + observer.onNext(calls); + } + return calls + 1; + }}, + new Action1() { + @Override + public void call(Integer t) { + countUnsubscribe.incrementAndGet(); + }}); + + List> subs = new ArrayList>(numSubscribers); + List> mocks = new ArrayList>(numSubscribers); + for (int i = 0; i < numSubscribers; i++) { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + TestSubscriber ts = new TestSubscriber(o); + subs.add(ts); + mocks.add(o); + } + + Observable o2 = Observable.create(os).subscribeOn(Schedulers.newThread()); + for (Subscriber ts : subs) { + o2.subscribe(ts); + } + + for (TestSubscriber ts : subs) { + ts.awaitTerminalEventAndUnsubscribeOnTimeout(1, TimeUnit.SECONDS); + } + + for (Observer o : mocks) { + verify(o, never()).onError(any(Throwable.class)); + verify(o, times(count)).onNext(any()); + verify(o, times(1)).onCompleted(); + } + assertEquals(numSubscribers, countUnsubscribe.get()); + } + + @Test(timeout = 10000) + public void testObserveOn() { + final int start = 1; + final int count = 4000; + + @SuppressWarnings("unchecked") + Action1 onUnSubscribe = mock(Action1.class); + @SuppressWarnings("unchecked") + Func0 generator = mock(Func0.class); + Mockito.when(generator.call()).thenReturn(start); + + OnSubscribe os = SyncOnSubscribe.createStateful(generator, + new Func2, Integer>() { + @Override + public Integer call(Integer calls, Observer observer) { + observer.onNext(calls); + if (calls == count) + observer.onCompleted(); + return calls + 1; + }}, + onUnSubscribe); + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + TestSubscriber ts = new TestSubscriber(o); + + TestScheduler scheduler = new TestScheduler(); + Observable.create(os).observeOn(scheduler).subscribe(ts); + + scheduler.triggerActions(); + ts.awaitTerminalEvent(); + + verify(o, never()).onError(any(Throwable.class)); + verify(o, times(count)).onNext(any(Integer.class)); + verify(o).onCompleted(); + verify(generator, times(1)).call(); + + List events = ts.getOnNextEvents(); + for (int i = 0; i < events.size(); i++) { + assertEquals(i + 1, events.get(i)); + } + verify(onUnSubscribe, times(1)).call(any(Integer.class)); + } + + @Test + public void testCanRequestInOnNext() { + Action0 onUnSubscribe = mock(Action0.class); + + OnSubscribe os = SyncOnSubscribe.createStateless( + new Action1>() { + @Override + public void call(Observer observer) { + observer.onNext(1); + observer.onCompleted(); + }}, + onUnSubscribe); + final AtomicReference exception = new AtomicReference(); + Observable.create(os).subscribe(new Subscriber() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + exception.set(e); + } + + @Override + public void onNext(Integer t) { + request(1); + } + }); + if (exception.get() != null) { + exception.get().printStackTrace(); + } + assertNull(exception.get()); + verify(onUnSubscribe, times(1)).call(); + } + + @Test + public void testExtendingBase() { + final AtomicReference lastState = new AtomicReference(); + final AtomicInteger countUnsubs = new AtomicInteger(0); + SyncOnSubscribe sos = new SyncOnSubscribe() { + @Override + protected Object generateState() { + Object o = new Object(); + lastState.set(o); + return o; + } + + @Override + protected Object next(Object state, Observer observer) { + observer.onNext(lastState.get()); + assertEquals(lastState.get(), state); + Object o = new Object(); + lastState.set(o); + return o; + } + + @Override + protected void onUnsubscribe(Object state) { + countUnsubs.incrementAndGet(); + assertEquals(lastState.get(), state); + } + }; + + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + TestSubscriber ts = new TestSubscriber(o); + + int count = 10; + Observable.create(sos).take(count).subscribe(ts); + + verify(o, never()).onError(any(Throwable.class)); + verify(o, times(count)).onNext(any(Object.class)); + verify(o).onCompleted(); + assertEquals(1, countUnsubs.get()); + } + + private interface FooQux {} + private static class Foo implements FooQux {} + private interface BarQux extends FooQux {} + private static class Bar extends Foo implements BarQux {} + + @Test + public void testGenericsCreateSingleState() { + Func0 generator = new Func0() { + @Override + public Bar call() { + return new Bar(); + }}; + Action2> next = new Action2>() { + @Override + public void call(BarQux state, Observer observer) { + observer.onNext(state); + observer.onCompleted(); + }}; + assertJustBehavior(SyncOnSubscribe.createSingleState(generator, next)); + } + + @Test + public void testGenericsCreateSingleStateWithUnsub() { + Func0 generator = new Func0() { + @Override + public Bar call() { + return new Bar(); + }}; + Action2> next = new Action2>() { + @Override + public void call(BarQux state, Observer observer) { + observer.onNext(state); + observer.onCompleted(); + }}; + Action1 unsub = new Action1() { + @Override + public void call(FooQux t) { + + }}; + assertJustBehavior(SyncOnSubscribe.createSingleState(generator, next, unsub)); + } + + @Test + public void testGenericsCreateStateful() { + Func0 generator = new Func0() { + @Override + public Bar call() { + return new Bar(); + }}; + Func2, ? extends BarQux> next = new Func2, BarQux>() { + @Override + public BarQux call(BarQux state, Observer observer) { + observer.onNext(state); + observer.onCompleted(); + return state; + }}; + assertJustBehavior(SyncOnSubscribe.createStateful(generator, next)); + } + + @Test + public void testGenericsCreateStatefulWithUnsub() { + Func0 generator = new Func0() { + @Override + public Bar call() { + return new Bar(); + }}; + Func2, ? extends BarQux> next = new Func2, BarQux>() { + @Override + public BarQux call(BarQux state, Observer observer) { + observer.onNext(state); + observer.onCompleted(); + return state; + }}; + Action1 unsub = new Action1() { + @Override + public void call(FooQux t) { + + }}; + OnSubscribe os = SyncOnSubscribe.createStateful(generator, next, unsub); + assertJustBehavior(os); + } + + @Test + public void testGenericsCreateStateless() { + Action1> next = new Action1>() { + @Override + public void call(Observer observer) { + observer.onNext(new Foo()); + observer.onCompleted(); + }}; + OnSubscribe os = SyncOnSubscribe.createStateless(next); + assertJustBehavior(os); + } + + @Test + public void testGenericsCreateStatelessWithUnsub() { + Action1> next = new Action1>() { + @Override + public void call(Observer observer) { + observer.onNext(new Foo()); + observer.onCompleted(); + }}; + Action0 unsub = new Action0() { + @Override + public void call() { + + }}; + OnSubscribe os = SyncOnSubscribe.createStateless(next, unsub); + assertJustBehavior(os); + } + + private void assertJustBehavior(OnSubscribe os) { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + + TestSubscriber ts = new TestSubscriber(o); + + os.call(ts); + verify(o, times(1)).onNext(any()); + verify(o, times(1)).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } +} From 583222d4b3aa81d3baf1751a9f9a94bed15a981b Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Wed, 2 Sep 2015 12:16:03 -0700 Subject: [PATCH 378/857] Fixing concurrent unsubscribe case of SyncOnSubscribe --- .../java/rx/observables/SyncOnSubscribe.java | 74 +++--- .../rx/observables/SyncOnSubscribeTest.java | 214 ++++++++++-------- 2 files changed, 156 insertions(+), 132 deletions(-) diff --git a/src/main/java/rx/observables/SyncOnSubscribe.java b/src/main/java/rx/observables/SyncOnSubscribe.java index 47a6c34024..c75173a094 100644 --- a/src/main/java/rx/observables/SyncOnSubscribe.java +++ b/src/main/java/rx/observables/SyncOnSubscribe.java @@ -16,7 +16,6 @@ package rx.observables; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLong; import rx.Observable.OnSubscribe; @@ -321,14 +320,9 @@ private static class SubscriptionProducer private final SyncOnSubscribe parent; private boolean onNextCalled; private boolean hasTerminated; - + private S state; - volatile int isUnsubscribed; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater IS_UNSUBSCRIBED = - AtomicIntegerFieldUpdater.newUpdater(SubscriptionProducer.class, "isUnsubscribed"); - private SubscriptionProducer(final Subscriber subscriber, SyncOnSubscribe parent, S state) { this.actualSubscriber = subscriber; this.parent = parent; @@ -337,14 +331,39 @@ private SubscriptionProducer(final Subscriber subscriber, SyncOnSubsc @Override public boolean isUnsubscribed() { - return isUnsubscribed != 0; + return get() < 0L; } @Override public void unsubscribe() { - IS_UNSUBSCRIBED.compareAndSet(this, 0, 1); - if (get() == 0L) - parent.onUnsubscribe(state); + while(true) { + long requestCount = get(); + if (compareAndSet(0L, -1L)) { + doUnsubscribe(); + return; + } + else if (compareAndSet(requestCount, -2L)) + // the loop is iterating concurrently + // need to check if requestCount == -1 + // and unsub if so after loop iteration + return; + } + } + + private boolean tryUnsubscribe() { + // only one thread at a time can iterate over request count + // therefore the requestCount atomic cannot be decrement concurrently here + // safe to set to -1 atomically (since this check can only be done by 1 thread) + if (hasTerminated || get() < -1) { + set(-1); + doUnsubscribe(); + return true; + } + return false; + } + + private void doUnsubscribe() { + parent.onUnsubscribe(state); } @Override @@ -358,71 +377,60 @@ public void request(long n) { } } - void fastpath() { + private void fastpath() { final SyncOnSubscribe p = parent; Subscriber a = actualSubscriber; - if (isUnsubscribed()) { - p.onUnsubscribe(state); - return; - } - for (;;) { try { onNextCalled = false; nextIteration(p); } catch (Throwable ex) { - handleThrownError(p, a, state, ex); + handleThrownError(a, ex); return; } - if (hasTerminated || isUnsubscribed()) { - p.onUnsubscribe(state); + if (tryUnsubscribe()) { return; } } } - private void handleThrownError(final SyncOnSubscribe p, Subscriber a, S st, Throwable ex) { + private void handleThrownError(Subscriber a, Throwable ex) { if (hasTerminated) { RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); } else { hasTerminated = true; a.onError(ex); - p.onUnsubscribe(st); + unsubscribe(); } } - void slowPath(long n) { + private void slowPath(long n) { final SyncOnSubscribe p = parent; Subscriber a = actualSubscriber; long numRequested = n; for (;;) { - if (isUnsubscribed()) { - p.onUnsubscribe(state); - return; - } long numRemaining = numRequested; do { try { onNextCalled = false; nextIteration(p); } catch (Throwable ex) { - handleThrownError(p, a, state, ex); + handleThrownError(a, ex); return; } - if (hasTerminated || isUnsubscribed()) { - p.onUnsubscribe(state); + if (tryUnsubscribe()) { return; } if (onNextCalled) numRemaining--; } while (numRemaining != 0L); - numRequested = addAndGet(-numRequested); - if (numRequested == 0L) { + if (numRequested <= 0L) break; - } } + // catches cases where unsubscribe is called before decrementing atomic request count + tryUnsubscribe(); } private void nextIteration(final SyncOnSubscribe parent) { diff --git a/src/test/java/rx/observables/SyncOnSubscribeTest.java b/src/test/java/rx/observables/SyncOnSubscribeTest.java index 91421d502a..22e1f11cfd 100644 --- a/src/test/java/rx/observables/SyncOnSubscribeTest.java +++ b/src/test/java/rx/observables/SyncOnSubscribeTest.java @@ -17,9 +17,9 @@ package rx.observables; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertFalse; import static org.mockito.Matchers.any; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.inOrder; @@ -33,8 +33,15 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -65,6 +72,7 @@ * Test if SyncOnSubscribe adheres to the usual unsubscription and backpressure contracts. */ public class SyncOnSubscribeTest { + @Test public void testObservableJustEquivalent() { OnSubscribe os = SyncOnSubscribe.createStateless(new Action1>() { @@ -91,13 +99,14 @@ public void testStateAfterTerminal() { public Integer call() { return 1; }}, - new Func2, Integer>() { - @Override - public Integer call(Integer state, Observer subscriber) { - subscriber.onNext(state); - subscriber.onCompleted(); - return state + 1; - }}, new Action1() { + new Func2, Integer>() { + @Override + public Integer call(Integer state, Observer subscriber) { + subscriber.onNext(state); + subscriber.onCompleted(); + return state + 1; + }}, + new Action1() { @Override public void call(Integer t) { finalStateValue.set(t); @@ -438,25 +447,14 @@ public Integer call(Integer state, Observer observer) { }}, onUnSubscribe); - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - TestSubscriber ts = new TestSubscriber(o) { - @Override - public void onStart() { - requestMore(0); // don't start right away - } - }; - + TestSubscriber ts = new TestSubscriber(0); Observable.create(os).subscribe(ts); ts.requestMore(finalCount); - verify(o, never()).onError(any(Throwable.class)); - verify(o, never()).onCompleted(); - inOrder.verify(o, times(finalCount)).onNext(any()); - inOrder.verifyNoMoreInteractions(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + ts.assertValueCount(finalCount); // unsubscribe does not take place because subscriber is still in process of requesting verify(onUnSubscribe, never()).call(any(Integer.class)); } @@ -485,31 +483,7 @@ public Integer call(Integer state, Observer observer) { TestSubscriber ts = new TestSubscriber(o); - Observable.create(os).lift(new Operator(){ - @Override - public Subscriber call(final Subscriber subscriber) { - return new Subscriber(){ - @Override - public void setProducer(Producer p) { - p.request(Long.MAX_VALUE); - } - - @Override - public void onCompleted() { - subscriber.onCompleted(); - } - - @Override - public void onError(Throwable e) { - subscriber.onError(e); - } - - @Override - public void onNext(Object t) { - subscriber.onNext(t); - unsubscribe(); - }}; - }}).take(1).subscribe(ts); + Observable.create(os).take(1).subscribe(ts); verify(o, never()).onError(any(Throwable.class)); verify(onUnSubscribe, times(1)).call(any(Integer.class)); @@ -577,27 +551,21 @@ public Integer call(Integer state, Observer observer) { } @Test - public void testUnsubscribeOutsideOfLoop() { + public void testUnsubscribeOutsideOfLoop() throws InterruptedException { final AtomicInteger calledUnsubscribe = new AtomicInteger(0); final AtomicBoolean currentlyEvaluating = new AtomicBoolean(false); - OnSubscribe os = SyncOnSubscribe.createStateful( - new Func0() { - @Override - public Void call() { - return null; - }}, - new Func2, Void>() { + OnSubscribe os = SyncOnSubscribe.createStateless( + new Action1>() { @Override - public Void call(Void state, Observer observer) { + public void call(Observer observer) { currentlyEvaluating.set(true); observer.onNext(null); currentlyEvaluating.set(false); - return null; }}, - new Action1(){ + new Action0(){ @Override - public void call(Void t) { + public void call() { calledUnsubscribe.incrementAndGet(); assertFalse(currentlyEvaluating.get()); }}); @@ -605,16 +573,12 @@ public void call(Void t) { @SuppressWarnings("unchecked") Observer o = mock(Observer.class); - final TestSubscriber ts = new TestSubscriber(o) { - @Override - public void onStart() { - requestMore(1); - } - }; + final CountDownLatch latch = new CountDownLatch(1); + final TestSubscriber ts = new TestSubscriber(o); Observable.create(os).lift(new Operator(){ @Override public Subscriber call(final Subscriber subscriber) { - return new Subscriber(){ + return new Subscriber(subscriber){ @Override public void setProducer(Producer p) { p.request(1); @@ -631,16 +595,23 @@ public void onError(Throwable e) { @Override public void onNext(final Void t) { + subscriber.onNext(t); new Thread(new Runnable(){ @Override public void run() { - subscriber.onNext(t); + try { + latch.await(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } unsubscribe(); subscriber.onCompleted(); + latch.countDown(); }}).start(); }}; }}).subscribe(ts); - ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + latch.countDown(); + ts.awaitTerminalEventAndUnsubscribeOnTimeout(1, TimeUnit.SECONDS); ts.assertNoErrors(); ts.assertUnsubscribed(); assertEquals(1, calledUnsubscribe.get()); @@ -708,29 +679,23 @@ public void call(Integer t) { }}); List> subs = new ArrayList>(numSubscribers); - List> mocks = new ArrayList>(numSubscribers); for (int i = 0; i < numSubscribers; i++) { - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - TestSubscriber ts = new TestSubscriber(o); + TestSubscriber ts = new TestSubscriber(); subs.add(ts); - mocks.add(o); } - - Observable o2 = Observable.create(os).subscribeOn(Schedulers.newThread()); + TestScheduler scheduler = new TestScheduler(); + Observable o2 = Observable.create(os).subscribeOn(scheduler); for (Subscriber ts : subs) { o2.subscribe(ts); } - + scheduler.triggerActions(); for (TestSubscriber ts : subs) { - ts.awaitTerminalEventAndUnsubscribeOnTimeout(1, TimeUnit.SECONDS); + ts.awaitTerminalEvent(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertValueCount(count); + ts.assertCompleted(); } - for (Observer o : mocks) { - verify(o, never()).onError(any(Throwable.class)); - verify(o, times(count)).onNext(any()); - verify(o, times(1)).onCompleted(); - } assertEquals(numSubscribers, countUnsubscribe.get()); } @@ -756,20 +721,16 @@ public Integer call(Integer calls, Observer observer) { }}, onUnSubscribe); - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - TestSubscriber ts = new TestSubscriber(o); + TestSubscriber ts = new TestSubscriber(); TestScheduler scheduler = new TestScheduler(); Observable.create(os).observeOn(scheduler).subscribe(ts); scheduler.triggerActions(); ts.awaitTerminalEvent(); - - verify(o, never()).onError(any(Throwable.class)); - verify(o, times(count)).onNext(any(Integer.class)); - verify(o).onCompleted(); + ts.assertNoErrors(); + ts.assertCompleted(); + ts.assertValueCount(count); verify(generator, times(1)).call(); List events = ts.getOnNextEvents(); @@ -969,14 +930,69 @@ public void call() { } private void assertJustBehavior(OnSubscribe os) { - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - TestSubscriber ts = new TestSubscriber(o); - + TestSubscriber ts = new TestSubscriber(); os.call(ts); - verify(o, times(1)).onNext(any()); - verify(o, times(1)).onCompleted(); - verify(o, never()).onError(any(Throwable.class)); + ts.assertCompleted(); + ts.assertNoErrors(); + ts.assertValueCount(1); + } + + @Test + public void testConcurrentUnsubscribe3000Iterations() throws InterruptedException, BrokenBarrierException, ExecutionException{ + ExecutorService exec = null; + try { + exec = Executors.newSingleThreadExecutor(); + for (int i = 0; i < 3000; i++) { + final AtomicInteger wip = new AtomicInteger(); + + Func0 func0 = new Func0() { + @Override + public AtomicInteger call() { + return wip; + } + }; + Func2, AtomicInteger> func2 = + new Func2, AtomicInteger>() { + @Override + public AtomicInteger call(AtomicInteger s, Observer o) { + o.onNext(1); + return s; + } + }; + Action1 action1 = new Action1() { + @Override + public void call(AtomicInteger s) { + s.getAndIncrement(); + } + }; + Observable source = Observable.create( + SyncOnSubscribe.createStateful( + func0, + func2, action1 + )); + + + final TestSubscriber ts = TestSubscriber.create(0); + source.subscribe(ts); + + final CyclicBarrier cb = new CyclicBarrier(2); + + Future f = exec.submit(new Callable() { + @Override + public Object call() throws Exception { + cb.await(); + ts.requestMore(1); + return null; + } + }); + + cb.await(); + ts.unsubscribe(); + f.get(); + assertEquals("Unsubscribe supposed to be called once", 1, wip.get()); + } + } finally { + if (exec != null) exec.shutdownNow(); + } } } From b6564c431dbbcb067a7c86880ab0a54cd5e10c5f Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 11 Sep 2015 14:05:34 +0200 Subject: [PATCH 379/857] test/subjects: Use statically imported never() methods ... and remove the unused Mockito imports --- .../java/rx/subjects/AsyncSubjectTest.java | 33 +++++++++---------- .../java/rx/subjects/BehaviorSubjectTest.java | 15 ++++----- .../java/rx/subjects/PublishSubjectTest.java | 17 +++++----- .../java/rx/subjects/ReplaySubjectTest.java | 20 +++++------ 4 files changed, 40 insertions(+), 45 deletions(-) diff --git a/src/test/java/rx/subjects/AsyncSubjectTest.java b/src/test/java/rx/subjects/AsyncSubjectTest.java index c4287fc363..623cdceb3f 100644 --- a/src/test/java/rx/subjects/AsyncSubjectTest.java +++ b/src/test/java/rx/subjects/AsyncSubjectTest.java @@ -33,7 +33,6 @@ import org.junit.Test; import org.mockito.InOrder; -import org.mockito.Mockito; import rx.Observer; import rx.Subscription; @@ -59,9 +58,9 @@ public void testNeverCompleted() { subject.onNext("two"); subject.onNext("three"); - verify(observer, Mockito.never()).onNext(anyString()); - verify(observer, Mockito.never()).onError(testException); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onNext(anyString()); + verify(observer, never()).onError(testException); + verify(observer, never()).onCompleted(); } @Test @@ -78,7 +77,7 @@ public void testCompleted() { subject.onCompleted(); verify(observer, times(1)).onNext("three"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @@ -94,7 +93,7 @@ public void testNull() { subject.onCompleted(); verify(observer, times(1)).onNext(null); - verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @@ -113,7 +112,7 @@ public void testSubscribeAfterCompleted() { subject.subscribe(observer); verify(observer, times(1)).onNext("three"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @@ -134,8 +133,8 @@ public void testSubscribeAfterError() { subject.subscribe(observer); verify(observer, times(1)).onError(re); - verify(observer, Mockito.never()).onNext(any(String.class)); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onNext(any(String.class)); + verify(observer, never()).onCompleted(); } @Test @@ -154,9 +153,9 @@ public void testError() { subject.onError(new Throwable()); subject.onCompleted(); - verify(observer, Mockito.never()).onNext(anyString()); + verify(observer, never()).onNext(anyString()); verify(observer, times(1)).onError(testException); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onCompleted(); } @Test @@ -172,16 +171,16 @@ public void testUnsubscribeBeforeCompleted() { subscription.unsubscribe(); - verify(observer, Mockito.never()).onNext(anyString()); - verify(observer, Mockito.never()).onError(any(Throwable.class)); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onNext(anyString()); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onCompleted(); subject.onNext("three"); subject.onCompleted(); - verify(observer, Mockito.never()).onNext(anyString()); - verify(observer, Mockito.never()).onError(any(Throwable.class)); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onNext(anyString()); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onCompleted(); } @Test diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index c64afa4efb..f577fa7c37 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -32,7 +32,6 @@ import org.junit.*; import org.mockito.InOrder; -import org.mockito.Mockito; import rx.*; import rx.exceptions.CompositeException; @@ -62,8 +61,8 @@ public void testThatObserverReceivesDefaultValueAndSubsequentEvents() { verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); - verify(observer, Mockito.never()).onError(testException); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onError(testException); + verify(observer, never()).onCompleted(); } @Test @@ -79,12 +78,12 @@ public void testThatObserverReceivesLatestAndThenSubsequentEvents() { subject.onNext("two"); subject.onNext("three"); - verify(observer, Mockito.never()).onNext("default"); + verify(observer, never()).onNext("default"); verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); - verify(observer, Mockito.never()).onError(testException); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onError(testException); + verify(observer, never()).onCompleted(); } @Test @@ -100,7 +99,7 @@ public void testSubscribeThenOnComplete() { verify(observer, times(1)).onNext("default"); verify(observer, times(1)).onNext("one"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @@ -116,7 +115,7 @@ public void testSubscribeToCompletedOnlyEmitsOnComplete() { verify(observer, never()).onNext("default"); verify(observer, never()).onNext("one"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } diff --git a/src/test/java/rx/subjects/PublishSubjectTest.java b/src/test/java/rx/subjects/PublishSubjectTest.java index f8bc989112..44fe824a5c 100644 --- a/src/test/java/rx/subjects/PublishSubjectTest.java +++ b/src/test/java/rx/subjects/PublishSubjectTest.java @@ -32,7 +32,6 @@ import org.junit.Test; import org.mockito.InOrder; -import org.mockito.Mockito; import rx.Observable; import rx.Observer; @@ -118,7 +117,7 @@ private void assertCompletedObserver(Observer observer) { verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @@ -152,7 +151,7 @@ private void assertErrorObserver(Observer observer) { verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); verify(observer, times(1)).onError(testException); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onCompleted(); } @Test @@ -180,10 +179,10 @@ public void testSubscribeMidSequence() { } private void assertCompletedStartingWithThreeObserver(Observer observer) { - verify(observer, Mockito.never()).onNext("one"); - verify(observer, Mockito.never()).onNext("two"); + verify(observer, never()).onNext("one"); + verify(observer, never()).onNext("two"); verify(observer, times(1)).onNext("three"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @@ -215,9 +214,9 @@ public void testUnsubscribeFirstObserver() { private void assertObservedUntilTwo(Observer observer) { verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); - verify(observer, Mockito.never()).onNext("three"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onCompleted(); } @Test diff --git a/src/test/java/rx/subjects/ReplaySubjectTest.java b/src/test/java/rx/subjects/ReplaySubjectTest.java index bd01901bc1..5ebb871604 100644 --- a/src/test/java/rx/subjects/ReplaySubjectTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectTest.java @@ -36,7 +36,6 @@ import org.junit.Test; import org.mockito.InOrder; -import org.mockito.Mockito; import rx.Observable; import rx.Observer; @@ -140,11 +139,10 @@ public void testCompletedStopsEmittingData() { inOrderD.verify(observerD).onNext(4711); inOrderD.verify(observerD).onCompleted(); - Mockito.verifyNoMoreInteractions(observerA); - Mockito.verifyNoMoreInteractions(observerB); - Mockito.verifyNoMoreInteractions(observerC); - Mockito.verifyNoMoreInteractions(observerD); - + verifyNoMoreInteractions(observerA); + verifyNoMoreInteractions(observerB); + verifyNoMoreInteractions(observerC); + verifyNoMoreInteractions(observerD); } @Test @@ -172,7 +170,7 @@ private void assertCompletedObserver(Observer observer) { inOrder.verify(observer, times(1)).onNext("one"); inOrder.verify(observer, times(1)).onNext("two"); inOrder.verify(observer, times(1)).onNext("three"); - inOrder.verify(observer, Mockito.never()).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onError(any(Throwable.class)); inOrder.verify(observer, times(1)).onCompleted(); inOrder.verifyNoMoreInteractions(); } @@ -206,7 +204,7 @@ private void assertErrorObserver(Observer observer) { verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); verify(observer, times(1)).onError(testException); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onCompleted(); } @SuppressWarnings("unchecked") @@ -261,9 +259,9 @@ public void testUnsubscribeFirstObserver() { private void assertObservedUntilTwo(Observer observer) { verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); - verify(observer, Mockito.never()).onNext("three"); - verify(observer, Mockito.never()).onError(any(Throwable.class)); - verify(observer, Mockito.never()).onCompleted(); + verify(observer, never()).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onCompleted(); } @Test(timeout = 2000) From b2c0325883dd9b2681f30cf871f7b8d77ee6db03 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 11 Sep 2015 14:23:21 +0200 Subject: [PATCH 380/857] BehaviorSubjectTest: Fix verification in testCompletedAfterErrorIsNotSent3() --- src/test/java/rx/subjects/BehaviorSubjectTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index f577fa7c37..e520970d12 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -253,7 +253,7 @@ public void testCompletedAfterErrorIsNotSent3() { subject.subscribe(o2); verify(o2, times(1)).onCompleted(); verify(o2, never()).onNext(any()); - verify(observer, never()).onError(any(Throwable.class)); + verify(o2, never()).onError(any(Throwable.class)); } @Test(timeout = 1000) public void testUnsubscriptionCase() { From 17ba5995ce2b835446d2edebbd205602dbf9641b Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Fri, 11 Sep 2015 14:27:17 +0200 Subject: [PATCH 381/857] BehaviorSubjectTest: Simplify testUnsubscriptionCase() test --- .../java/rx/subjects/BehaviorSubjectTest.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index e520970d12..9e9e4c90e7 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -274,22 +274,8 @@ public Observable call(String t1) { return Observable.just(t1 + ", " + t1); } }) - .subscribe(new Observer() { - @Override - public void onNext(String t) { - o.onNext(t); - } - - @Override - public void onError(Throwable e) { - o.onError(e); - } + .subscribe(o); - @Override - public void onCompleted() { - o.onCompleted(); - } - }); inOrder.verify(o).onNext(v + ", " + v); inOrder.verify(o).onCompleted(); verify(o, never()).onError(any(Throwable.class)); From 70eea84c646644ef374784e0284c6cd113fa28c1 Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Wed, 26 Aug 2015 17:48:11 -0700 Subject: [PATCH 382/857] Implemented the AsyncOnSubscribe --- .../java/rx/observables/AsyncOnSubscribe.java | 510 ++++++++++++++++++ .../rx/observables/AsyncOnSubscribeTest.java | 408 ++++++++++++++ 2 files changed, 918 insertions(+) create mode 100644 src/main/java/rx/observables/AsyncOnSubscribe.java create mode 100644 src/test/java/rx/observables/AsyncOnSubscribeTest.java diff --git a/src/main/java/rx/observables/AsyncOnSubscribe.java b/src/main/java/rx/observables/AsyncOnSubscribe.java new file mode 100644 index 0000000000..d4a12b0245 --- /dev/null +++ b/src/main/java/rx/observables/AsyncOnSubscribe.java @@ -0,0 +1,510 @@ +/** + * Copyright 2015 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.observables; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Producer; +import rx.Subscriber; +import rx.Subscription; +import rx.annotations.Experimental; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Action3; +import rx.functions.Func0; +import rx.functions.Func3; +import rx.internal.operators.BufferUntilSubscriber; +import rx.observers.SerializedObserver; +import rx.observers.Subscribers; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.BooleanSubscription; +; +/** + * A utility class to create {@code OnSubscribe} functions that respond correctly to back + * pressure requests from subscribers. This is an improvement over + * {@link rx.Observable#create(OnSubscribe) Observable.create(OnSubscribe)} which does not provide + * any means of managing back pressure requests out-of-the-box. This variant of an OnSubscribe + * function allows for the asynchronous processing of requests. + * + * @param + * the type of the user-define state used in {@link #generateState() generateState(S)} , + * {@link #next(Object, Long, Subscriber) next(S, Long, Subscriber)}, and + * {@link #onUnsubscribe(Object) onUnsubscribe(S)}. + * @param + * the type of {@code Subscribers} that will be compatible with {@code this}. + */ +@Experimental +public abstract class AsyncOnSubscribe implements OnSubscribe { + + /** + * Executed once when subscribed to by a subscriber (via {@link OnSubscribe#call(Subscriber)}) + * to produce a state value. This value is passed into {@link #next(Object, long, Observer) + * next(S state, Observer observer)} on the first iteration. Subsequent iterations of + * {@code next} will receive the state returned by the previous invocation of {@code next}. + * + * @return the initial state value + */ + protected abstract S generateState(); + + /** + * Called to produce data to the downstream subscribers. To emit data to a downstream subscriber + * call {@code observer.onNext(t)}. To signal an error condition call + * {@code observer.onError(throwable)} or throw an Exception. To signal the end of a data stream + * call {@code observer.onCompleted()}. Implementations of this method must follow the following + * rules. + * + *
    + *
  • Must not call {@code observer.onNext(t)} more than 1 time per invocation.
  • + *
  • Must not call {@code observer.onNext(t)} concurrently.
  • + *
+ * + * The value returned from an invocation of this method will be passed in as the {@code state} + * argument of the next invocation of this method. + * + * @param state + * the state value (from {@link #generateState()} on the first invocation or the + * previous invocation of this method. + * @param requested + * the amount of data requested. An observable emitted to the observer should not + * exceed this amount. + * @param observer + * the observer of data emitted by + * @return the next iteration's state value + */ + protected abstract S next(S state, long requested, Observer> observer); + + /** + * Clean up behavior that is executed after the downstream subscriber's subscription is + * unsubscribed. This method will be invoked exactly once. + * + * @param state + * the last state value returned from {@code next(S, Long, Observer)} or + * {@code generateState()} at the time when a terminal event is emitted from + * {@link #next(Object, long, Observer)} or unsubscribing. + */ + protected void onUnsubscribe(S state) { + + } + + /** + * Generates a synchronous {@link AsyncOnSubscribe} that calls the provided {@code next} + * function to generate data to downstream subscribers. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @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. + */ + @Experimental + public static OnSubscribe createSingleState(Func0 generator, + final Action3>> next) { + Func3>, S> nextFunc = + new Func3>, S>() { + @Override + public S call(S state, Long requested, Observer> subscriber) { + next.call(state, requested, subscriber); + return state; + }}; + return new AsyncOnSubscribeImpl(generator, nextFunc); + } + + /** + * Generates a synchronous {@link AsyncOnSubscribe} that calls the provided {@code next} + * function to generate data to downstream subscribers. + * + * This overload creates a AsyncOnSubscribe without an explicit clean up step. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @param next + * produces data to the downstream subscriber (see + * {@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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createSingleState(Func0 generator, + final Action3>> next, + final Action1 onUnsubscribe) { + Func3>, S> nextFunc = + new Func3>, S>() { + @Override + public S call(S state, Long requested, Observer> subscriber) { + next.call(state, requested, subscriber); + return state; + }}; + return new AsyncOnSubscribeImpl(generator, nextFunc, onUnsubscribe); + } + + /** + * Generates a synchronous {@link AsyncOnSubscribe} that calls the provided {@code next} + * function to generate data to downstream subscribers. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @param next + * produces data to the downstream subscriber (see + * {@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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateful(Func0 generator, + Func3>, ? extends S> next, + Action1 onUnsubscribe) { + return new AsyncOnSubscribeImpl(generator, next, onUnsubscribe); + } + + /** + * Generates a synchronous {@link AsyncOnSubscribe} that calls the provided {@code next} + * function to generate data to downstream subscribers. + * + * @param generator + * generates the initial state value (see {@link #generateState()}) + * @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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateful(Func0 generator, + Func3>, ? extends S> next) { + return new AsyncOnSubscribeImpl(generator, next); + } + + /** + * Generates a synchronous {@link AsyncOnSubscribe} that calls the provided {@code next} + * function to generate data to downstream subscribers. + * + * This overload creates a "state-less" AsyncOnSubscribe which does not have an explicit state + * value. This should be used when the {@code next} function closes over it'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 downstream in a protocol compatible with + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateless(final Action2>> next) { + Func3>, Void> nextFunc = + new Func3>, Void>() { + @Override + public Void call(Void state, Long requested, Observer> subscriber) { + next.call(requested, subscriber); + return state; + }}; + return new AsyncOnSubscribeImpl(nextFunc); + } + + /** + * Generates a synchronous {@link AsyncOnSubscribe} that calls the provided {@code next} + * function to generate data to downstream subscribers. + * + * This overload creates a "state-less" AsyncOnSubscribe which does not have an explicit state + * value. This should be used when the {@code next} function closes over it's state. + * + * @param next + * produces data to the downstream subscriber (see + * {@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 + * back-pressure. + */ + @Experimental + public static OnSubscribe createStateless(final Action2>> next, + final Action0 onUnsubscribe) { + Func3>, Void> nextFunc = + new Func3>, Void>() { + @Override + public Void call(Void state, Long requested, Observer> subscriber) { + next.call(requested, subscriber); + return null; + }}; + Action1 wrappedOnUnsubscribe = new Action1() { + @Override + public void call(Void t) { + onUnsubscribe.call(); + }}; + return new AsyncOnSubscribeImpl(nextFunc, wrappedOnUnsubscribe); + } + + /** + * An implementation of AsyncOnSubscribe that delegates + * {@link AsyncOnSubscribe#next(Object, long, Observer)}, + * {@link AsyncOnSubscribe#generateState()}, and {@link AsyncOnSubscribe#onUnsubscribe(Object)} + * to provided functions/closures. + * + * @param + * the type of the user-defined state + * @param + * the type of compatible Subscribers + */ + private static final class AsyncOnSubscribeImpl extends AsyncOnSubscribe { + private final Func0 generator; + private final Func3>, ? extends S> next; + private final Action1 onUnsubscribe; + + private AsyncOnSubscribeImpl(Func0 generator, Func3>, ? extends S> next, Action1 onUnsubscribe) { + this.generator = generator; + this.next = next; + this.onUnsubscribe = onUnsubscribe; + } + + public AsyncOnSubscribeImpl(Func0 generator, Func3>, ? extends S> next) { + this(generator, next, null); + } + + public AsyncOnSubscribeImpl(Func3>, S> next, Action1 onUnsubscribe) { + this(null, next, onUnsubscribe); + } + + public AsyncOnSubscribeImpl(Func3>, S> nextFunc) { + this(null, nextFunc, null); + } + + @Override + protected S generateState() { + return generator == null ? null : generator.call(); + } + + @Override + protected S next(S state, long requested, Observer> observer) { + return next.call(state, requested, observer); + } + + @Override + protected void onUnsubscribe(S state) { + if (onUnsubscribe != null) + onUnsubscribe.call(state); + } + } + + @Override + public final void call(Subscriber actualSubscriber) { + S state = generateState(); + UnicastSubject> subject = UnicastSubject.> create(); + AsyncOuterSubscriber outerSubscriberProducer = new AsyncOuterSubscriber(this, state, subject); + actualSubscriber.add(outerSubscriberProducer); + Observable.concat(subject).unsafeSubscribe(Subscribers.wrap(actualSubscriber)); + actualSubscriber.setProducer(outerSubscriberProducer); + } + + private static class AsyncOuterSubscriber extends ConcurrentLinkedQueueimplements Producer, Subscription, Observer> { + /** */ + private static final long serialVersionUID = -7884904861928856832L; + + private volatile int isUnsubscribed; + @SuppressWarnings("rawtypes") + private static final AtomicIntegerFieldUpdater IS_UNSUBSCRIBED = AtomicIntegerFieldUpdater.newUpdater(AsyncOuterSubscriber.class, "isUnsubscribed"); + + private final AsyncOnSubscribe parent; + private final SerializedObserver> serializedSubscriber; + private final Set subscriptions = new HashSet(); + + private boolean hasTerminated = false; + private boolean onNextCalled = false; + + private S state; + + private final UnicastSubject> merger; + + public AsyncOuterSubscriber(AsyncOnSubscribe parent, S initialState, UnicastSubject> merger) { + this.parent = parent; + this.serializedSubscriber = new SerializedObserver>(this); + this.state = initialState; + this.merger = merger; + } + + @Override + public void unsubscribe() { + if (IS_UNSUBSCRIBED.compareAndSet(this, 0, 1)) { + // it's safe to process terminal behavior + if (isEmpty()) { + parent.onUnsubscribe(state); + } + for (Subscription s : subscriptions) { + if (!s.isUnsubscribed()) { + s.unsubscribe(); + } + } + } + } + + @Override + public boolean isUnsubscribed() { + return isUnsubscribed != 0; + } + + public void nextIteration(long requestCount) { + state = parent.next(state, requestCount, serializedSubscriber); + } + + @Override + public void request(long n) { + int size = 0; + Long r; + synchronized (this) { + size = size(); + add(n); + r = n; + } + if (size == 0) { + do { + // check if unsubscribed before doing any work + if (isUnsubscribed()) { + unsubscribe(); + return; + } + // otherwise try one iteration for a request of `numRequested` elements + try { + onNextCalled = false; + nextIteration(r); + if (onNextCalled) + r = poll(); + if (hasTerminated || isUnsubscribed()) { + parent.onUnsubscribe(state); + } + } catch (Throwable ex) { + handleThrownError(parent, state, ex); + return; + } + } while (r != null && !hasTerminated); + } + } + + private void handleThrownError(final AsyncOnSubscribe p, S st, Throwable ex) { + if (hasTerminated) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); + } else { + hasTerminated = true; + merger.onError(ex); + unsubscribe(); + } + } + + @Override + public void onCompleted() { + if (hasTerminated) { + throw new IllegalStateException("Terminal event already emitted."); + } + hasTerminated = true; + merger.onCompleted(); + } + + @Override + public void onError(Throwable e) { + if (hasTerminated) { + throw new IllegalStateException("Terminal event already emitted."); + } + hasTerminated = true; + merger.onError(e); + } + + // This exists simply to check if the subscription has already been + // terminated before getting access to the subscription + private static Subscription SUBSCRIPTION_SENTINEL = new BooleanSubscription(); + + @Override + public void onNext(final Observable t) { + if (onNextCalled) { + throw new IllegalStateException("onNext called multiple times!"); + } + onNextCalled = true; + if (hasTerminated) + return; + subscribeBufferToObservable(t); + } + + private void subscribeBufferToObservable(final Observable t) { + BufferUntilSubscriber buffer = BufferUntilSubscriber. create(); + final AtomicReference holder = new AtomicReference(null); + Subscription innerSubscription = t + .doOnTerminate(new Action0() { + @Override + public void call() { + if (!holder.compareAndSet(null, SUBSCRIPTION_SENTINEL)) { + Subscription h = holder.get(); + subscriptions.remove(h); + } + }}) + .subscribe(buffer); + + if (holder.compareAndSet(null, innerSubscription)) { + subscriptions.add(innerSubscription); + } + merger.onNext(buffer); + } + } + + private static final class UnicastSubject extends Observableimplements Observer { + public static UnicastSubject create() { + return new UnicastSubject(new State()); + } + + private State state; + + protected UnicastSubject(final State state) { + super(new OnSubscribe() { + @Override + public void call(Subscriber s) { + if (state.subscriber != null) { + s.onError(new IllegalStateException("There can be only one subscriber")); + } else { + state.subscriber = s; + } + } + }); + this.state = state; + } + + @Override + public void onCompleted() { + state.subscriber.onCompleted(); + } + + @Override + public void onError(Throwable e) { + state.subscriber.onError(e); + } + + @Override + public void onNext(T t) { + state.subscriber.onNext(t); + } + + private static class State { + private Subscriber subscriber; + } + } +} diff --git a/src/test/java/rx/observables/AsyncOnSubscribeTest.java b/src/test/java/rx/observables/AsyncOnSubscribeTest.java new file mode 100644 index 0000000000..92537dc455 --- /dev/null +++ b/src/test/java/rx/observables/AsyncOnSubscribeTest.java @@ -0,0 +1,408 @@ +package rx.observables; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Subscription; +import rx.exceptions.TestException; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Func0; +import rx.functions.Func3; +import rx.internal.util.RxRingBuffer; +import rx.observers.TestSubscriber; +import rx.schedulers.TestScheduler; + +@RunWith(MockitoJUnitRunner.class) +public class AsyncOnSubscribeTest { + + @Mock + public Observer o; + + TestSubscriber subscriber; + + @Before + public void setup() { + subscriber = new TestSubscriber(o); + } + + @Test + public void testSerializesConcurrentObservables() throws InterruptedException { + final TestScheduler scheduler = new TestScheduler(); + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + if (state == 1) { + Observable o1 = Observable.just(1, 2, 3, 4) + .delay(100, TimeUnit.MILLISECONDS, scheduler); + observer.onNext(o1); + } + else if (state == 2) { + Observable o = Observable.just(5, 6, 7, 8); + observer.onNext(o); + } + else + observer.onCompleted(); + return state + 1; + }}); + // initial request emits [[1, 2, 3, 4]] on delay + Observable.create(os).subscribe(subscriber); + // next request emits [[5, 6, 7, 8]] firing immediately + subscriber.requestMore(2); + // triggers delayed observable + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + // final request completes + subscriber.requestMore(3); + subscriber.awaitTerminalEventAndUnsubscribeOnTimeout(100, TimeUnit.MILLISECONDS); + subscriber.assertNoErrors(); + subscriber.assertReceivedOnNext(Arrays.asList(new Integer[] {1, 2, 3, 4, 5, 6, 7, 8})); + subscriber.assertCompleted(); + } + + @Test + public void testSubscribedByBufferingOperator() { + final TestScheduler scheduler = new TestScheduler(); + OnSubscribe os = AsyncOnSubscribe.createStateless( + new Action2>>(){ + @Override + public void call(Long requested, Observer> observer) { + observer.onNext(Observable.range(1, requested.intValue())); + }}); + Observable.create(os).observeOn(scheduler).subscribe(subscriber); + scheduler.advanceTimeBy(10, TimeUnit.DAYS); + subscriber.assertNoErrors(); + subscriber.assertValueCount(RxRingBuffer.SIZE); + subscriber.assertNotCompleted(); + } + + @Test + public void testOnUnsubscribeHasCorrectState() throws InterruptedException { + final AtomicInteger lastState = new AtomicInteger(-1); + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + if (state < 3) { + observer.onNext(Observable.just(state)); + } + else + observer.onCompleted(); + return state + 1; + }}, + new Action1() { + @Override + public void call(Integer t) { + lastState.set(t); + }}); + Observable.create(os).subscribe(subscriber); // [[1]], state = 1 + subscriber.requestMore(2); // [[1]], state = 2 + subscriber.requestMore(3); // onComplete, state = 3 + subscriber.assertNoErrors(); + subscriber.assertReceivedOnNext(Arrays.asList(new Integer[] {1, 2})); + subscriber.assertCompleted(); + assertEquals("Final state when unsubscribing is not correct", 4, lastState.get()); + } + + @Test + public void testOnCompleteOuter() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateless(new Action2>>(){ + @Override + public void call(Long requested, Observer> observer) { + observer.onCompleted(); + }}); + Observable.create(os).subscribe(subscriber); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + subscriber.assertNoValues(); + } + + @Test + public void testTryOnNextTwice() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateless(new Action2>>(){ + @Override + public void call(Long requested, Observer> observer) { + observer.onNext(Observable.just(1)); + observer.onNext(Observable.just(2)); + } + }); + Observable.create(os).subscribe(subscriber); + subscriber.assertError(IllegalStateException.class); + subscriber.assertNotCompleted(); + subscriber.assertReceivedOnNext(Arrays.asList(new Integer[] {1})); + } + + @Test + public void testThrowException() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateless( + new Action2>>(){ + @Override + public void call(Long requested, Observer> observer) { + throw new TestException(); + }}); + Observable.create(os).subscribe(subscriber); + subscriber.assertError(TestException.class); + subscriber.assertNotCompleted(); + subscriber.assertNoValues(); + } + + @Test + public void testThrowExceptionAfterTerminal() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + observer.onCompleted(); + throw new TestException(); + }}); + Observable.create(os).subscribe(subscriber); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + subscriber.assertNoValues(); + } + + @Test + public void testOnNextAfterCompleted() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + observer.onCompleted(); + observer.onNext(Observable.just(1)); + return 1; + }}); + Observable.create(os).subscribe(subscriber); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + subscriber.assertNoValues(); + } + + @Test + public void testOnNextAfterError() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + observer.onError(new TestException()); + observer.onNext(Observable.just(1)); + return 1; + }}); + Observable.create(os).subscribe(subscriber); + subscriber.assertError(TestException.class); + subscriber.assertNotCompleted(); + subscriber.assertNoValues(); + } + + @Test + public void testEmittingEmptyObservable() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + observer.onNext(Observable.empty()); + observer.onCompleted(); + return state; + }}); + Observable.create(os).subscribe(subscriber); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + subscriber.assertNoValues(); + } + + @Test + public void testOnErrorOuter() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + observer.onError(new TestException()); + return state; + } + }); + Observable.create(os).subscribe(subscriber); + subscriber.assertError(TestException.class); + subscriber.assertNotCompleted(); + subscriber.assertNoValues(); + } + + @Test + public void testOnCompleteFollowedByOnErrorOuter() throws InterruptedException { + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + observer.onCompleted(); + observer.onError(new TestException()); + return state; + } + }); + Observable.create(os).subscribe(subscriber); + subscriber.assertCompleted(); + subscriber.assertNoErrors(); + subscriber.assertNoValues(); + } + + @Test + public void testUnsubscribesFromAllSelfTerminatedObservables() throws InterruptedException { + final AtomicInteger l1 = new AtomicInteger(); + final AtomicInteger l2 = new AtomicInteger(); + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + Observable o1; + switch (state) { + case 1: + o1 = Observable.just(1) + .doOnUnsubscribe(new Action0(){ + @Override + public void call() { + l1.incrementAndGet(); + }}); + break; + case 2: + o1 = Observable.just(2) + .doOnUnsubscribe(new Action0(){ + @Override + public void call() { + l2.incrementAndGet(); + }}); + break; + default: + observer.onCompleted(); + return null; + } + observer.onNext(o1); + return state + 1; + }}); + Observable.create(os).subscribe(subscriber); // [[1]] + subscriber.requestMore(2); // [[2]] + subscriber.requestMore(2); // onCompleted + subscriber.awaitTerminalEventAndUnsubscribeOnTimeout(100, TimeUnit.MILLISECONDS); + assertEquals("did not unsub from first observable after terminal", 1, l1.get()); + assertEquals("did not unsub from second observable after terminal", 1, l2.get()); + List onNextEvents = subscriber.getOnNextEvents(); + assertEquals(2, onNextEvents.size()); + assertEquals(new Integer(1), onNextEvents.get(0)); + assertEquals(new Integer(2), onNextEvents.get(1)); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + } + + @Test + public void testUnsubscribesFromAllNonTerminatedObservables() throws InterruptedException { + final AtomicInteger l1 = new AtomicInteger(); + final AtomicInteger l2 = new AtomicInteger(); + final TestScheduler scheduler = new TestScheduler(); + final AtomicReference sub = new AtomicReference(); + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + switch (state) { + case 1: + observer.onNext(Observable.just(1) + .subscribeOn(scheduler) + .doOnUnsubscribe(new Action0(){ + @Override + public void call() { + l1.incrementAndGet(); + }})); + break; + case 2: + observer.onNext(Observable.never() + .subscribeOn(scheduler) + .doOnUnsubscribe(new Action0(){ + @Override + public void call() { + l2.incrementAndGet(); + }})); + break; + case 3: + sub.get().unsubscribe(); + } + return state + 1; + }}); + Subscription subscription = Observable.create(os) + .observeOn(scheduler) + .subscribe(subscriber); + sub.set(subscription); + subscriber.assertNoValues(); + scheduler.triggerActions(); + subscriber.assertValue(1); + subscriber.assertNotCompleted(); + subscriber.assertNoErrors(); + assertEquals("did not unsub from 1st observable after terminal", 1, l1.get()); + assertEquals("did not unsub from Observable.never() inner obs", 1, l2.get()); + } + + private static class Foo {} + private static class Bar extends Foo {} + + @Test + public void testGenerics() { + AsyncOnSubscribe.createStateless(new Action2>>(){ + @Override + public void call(Long state, Observer> observer) { + if (state == null) + observer.onNext(Observable.just(new Foo())); + else + observer.onNext(Observable.just(new Bar())); + }}); + } +} From 8d21e08d0627b591d5f4cb7c9694b983c90835e2 Mon Sep 17 00:00:00 2001 From: Kevin Coughlin Date: Fri, 11 Sep 2015 15:17:23 -0400 Subject: [PATCH 383/857] Remove unnecessary onStart in OperatorGroupBy --- src/main/java/rx/internal/operators/OperatorGroupBy.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index ffced4c923..02efb20f3f 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -259,9 +259,6 @@ public void call() { } }).unsafeSubscribe(new Subscriber(o) { - @Override - public void onStart() { - } @Override public void onCompleted() { o.onCompleted(); From f54252fc6eaf0445fbf45a576f0c63096d518b3f Mon Sep 17 00:00:00 2001 From: Victor Vu Date: Tue, 15 Sep 2015 21:04:20 -0600 Subject: [PATCH 384/857] Make BlockingOperatorToIterator exert backpressure. --- .../operators/BlockingOperatorToIterator.java | 115 ++++++++++-------- .../BlockingOperatorToIteratorTest.java | 42 +++++++ 2 files changed, 106 insertions(+), 51 deletions(-) diff --git a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java index 6f631a211d..7070436c42 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java @@ -23,7 +23,6 @@ import rx.Notification; import rx.Observable; import rx.Subscriber; -import rx.Subscription; import rx.exceptions.Exceptions; /** @@ -47,68 +46,82 @@ private BlockingOperatorToIterator() { * @return the iterator that could be used to iterate over the elements of the observable. */ public static Iterator toIterator(Observable source) { - final BlockingQueue> notifications = new LinkedBlockingQueue>(); + SubscriberIterator subscriber = new SubscriberIterator(); // using subscribe instead of unsafeSubscribe since this is a BlockingObservable "final subscribe" - final Subscription subscription = source.materialize().subscribe(new Subscriber>() { - @Override - public void onCompleted() { - // ignore - } + source.materialize().subscribe(subscriber); + return subscriber; + } - @Override - public void onError(Throwable e) { - notifications.offer(Notification.createOnError(e)); - } + public static final class SubscriberIterator + extends Subscriber> implements Iterator { - @Override - public void onNext(Notification args) { - notifications.offer(args); - } - }); + private final BlockingQueue> notifications; + private Notification buf; - return new Iterator() { - private Notification buf; + public SubscriberIterator() { + this.notifications = new LinkedBlockingQueue>(); + this.buf = null; + } - @Override - public boolean hasNext() { - if (buf == null) { - buf = take(); - } - if (buf.isOnError()) { - throw Exceptions.propagate(buf.getThrowable()); - } - return !buf.isOnCompleted(); + @Override + public void onStart() { + request(0); + } + + @Override + public void onCompleted() { + // ignore + } + + @Override + public void onError(Throwable e) { + notifications.offer(Notification.createOnError(e)); + } + + @Override + public void onNext(Notification args) { + notifications.offer(args); + } + + @Override + public boolean hasNext() { + if (buf == null) { + request(1); + buf = take(); + } + if (buf.isOnError()) { + throw Exceptions.propagate(buf.getThrowable()); } + return !buf.isOnCompleted(); + } - @Override - public T next() { - if (hasNext()) { - T result = buf.getValue(); - buf = null; - return result; - } - throw new NoSuchElementException(); + @Override + public T next() { + if (hasNext()) { + T result = buf.getValue(); + buf = null; + return result; } + throw new NoSuchElementException(); + } - private Notification take() { - try { - Notification poll = notifications.poll(); - if (poll != null) { - return poll; - } - return notifications.take(); - } catch (InterruptedException e) { - subscription.unsubscribe(); - throw Exceptions.propagate(e); + private Notification take() { + try { + Notification poll = notifications.poll(); + if (poll != null) { + return poll; } + return notifications.take(); + } catch (InterruptedException e) { + unsubscribe(); + throw Exceptions.propagate(e); } + } - @Override - public void remove() { - throw new UnsupportedOperationException("Read-only iterator"); - } - }; + @Override + public void remove() { + throw new UnsupportedOperationException("Read-only iterator"); + } } - } diff --git a/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java b/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java index db92042f1a..d8cf0aa9ed 100644 --- a/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java +++ b/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java @@ -81,4 +81,46 @@ public void call(Subscriber subscriber) { System.out.println(string); } } + + @Test + public void testIteratorExertBackpressure() { + final Counter src = new Counter(); + + Observable obs = Observable.from(new Iterable() { + @Override + public Iterator iterator() { + return src; + } + }); + + Iterator it = toIterator(obs); + while (it.hasNext()) { + // Correct backpressure should cause this interleaved behavior. + int i = it.next(); + assertEquals(i + 1, src.count); + } + } + + public static final class Counter implements Iterator { + public int count; + + public Counter() { + this.count = 0; + } + + @Override + public boolean hasNext() { + return count < 5; + } + + @Override + public Integer next() { + return count++; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } } From d1a8739186eea58f3e5efea0af08c39b8643b114 Mon Sep 17 00:00:00 2001 From: Victor Vu Date: Wed, 16 Sep 2015 02:20:50 -0600 Subject: [PATCH 385/857] Request data in batches. --- .../operators/BlockingOperatorToIterator.java | 13 ++++++++++--- .../BlockingOperatorToIteratorTest.java | 18 +++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java index 7070436c42..899aaffacb 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorToIterator.java @@ -24,6 +24,7 @@ import rx.Observable; import rx.Subscriber; import rx.exceptions.Exceptions; +import rx.internal.util.RxRingBuffer; /** * Returns an Iterator that iterates over all items emitted by a specified Observable. @@ -56,17 +57,19 @@ public static Iterator toIterator(Observable source) { public static final class SubscriberIterator extends Subscriber> implements Iterator { + static final int LIMIT = 3 * RxRingBuffer.SIZE / 4; + private final BlockingQueue> notifications; private Notification buf; + private int received; public SubscriberIterator() { this.notifications = new LinkedBlockingQueue>(); - this.buf = null; } @Override public void onStart() { - request(0); + request(RxRingBuffer.SIZE); } @Override @@ -87,8 +90,12 @@ public void onNext(Notification args) { @Override public boolean hasNext() { if (buf == null) { - request(1); buf = take(); + received++; + if (received >= LIMIT) { + request(received); + received = 0; + } } if (buf.isOnError()) { throw Exceptions.propagate(buf.getThrowable()); diff --git a/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java b/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java index d8cf0aa9ed..4ed030660b 100644 --- a/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java +++ b/src/test/java/rx/internal/operators/BlockingOperatorToIteratorTest.java @@ -26,6 +26,8 @@ import rx.Observable.OnSubscribe; import rx.Subscriber; import rx.exceptions.TestException; +import rx.internal.operators.BlockingOperatorToIterator.SubscriberIterator; +import rx.internal.util.RxRingBuffer; public class BlockingOperatorToIteratorTest { @@ -96,26 +98,28 @@ public Iterator iterator() { Iterator it = toIterator(obs); while (it.hasNext()) { // Correct backpressure should cause this interleaved behavior. + // We first request RxRingBuffer.SIZE. Then in increments of + // SubscriberIterator.LIMIT. int i = it.next(); - assertEquals(i + 1, src.count); + int expected = i - (i % SubscriberIterator.LIMIT) + RxRingBuffer.SIZE; + expected = Math.min(expected, Counter.MAX); + + assertEquals(expected, src.count); } } public static final class Counter implements Iterator { + static final int MAX = 5 * RxRingBuffer.SIZE; public int count; - public Counter() { - this.count = 0; - } - @Override public boolean hasNext() { - return count < 5; + return count < MAX; } @Override public Integer next() { - return count++; + return ++count; } @Override From ac00ffca41328ad291dd1287e04cd42d9c60680d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Sat, 19 Sep 2015 11:36:15 +0200 Subject: [PATCH 386/857] Fix to a bunch of bugs and issues with AsyncOnSubscribe --- .../java/rx/observables/AsyncOnSubscribe.java | 335 +++++++++++++----- .../rx/observables/AsyncOnSubscribeTest.java | 76 +++- 2 files changed, 312 insertions(+), 99 deletions(-) diff --git a/src/main/java/rx/observables/AsyncOnSubscribe.java b/src/main/java/rx/observables/AsyncOnSubscribe.java index d4a12b0245..84cb4c98e4 100644 --- a/src/main/java/rx/observables/AsyncOnSubscribe.java +++ b/src/main/java/rx/observables/AsyncOnSubscribe.java @@ -16,30 +16,19 @@ package rx.observables; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.*; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReference; +import rx.*; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observer; -import rx.Producer; -import rx.Subscriber; -import rx.Subscription; import rx.annotations.Experimental; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Action2; -import rx.functions.Action3; -import rx.functions.Func0; -import rx.functions.Func3; -import rx.internal.operators.BufferUntilSubscriber; -import rx.observers.SerializedObserver; -import rx.observers.Subscribers; +import rx.functions.*; +import rx.internal.operators.*; +import rx.observers.*; import rx.plugins.RxJavaPlugins; -import rx.subscriptions.BooleanSubscription; +import rx.subscriptions.CompositeSubscription; ; /** * A utility class to create {@code OnSubscribe} functions that respond correctly to back @@ -311,35 +300,77 @@ protected void onUnsubscribe(S state) { } @Override - public final void call(Subscriber actualSubscriber) { - S state = generateState(); + public final void call(final Subscriber actualSubscriber) { + S state; + try { + state = generateState(); + } catch (Throwable ex) { + actualSubscriber.onError(ex); + return; + } UnicastSubject> subject = UnicastSubject.> create(); - AsyncOuterSubscriber outerSubscriberProducer = new AsyncOuterSubscriber(this, state, subject); - actualSubscriber.add(outerSubscriberProducer); - Observable.concat(subject).unsafeSubscribe(Subscribers.wrap(actualSubscriber)); - actualSubscriber.setProducer(outerSubscriberProducer); + + final AsyncOuterManager outerProducer = new AsyncOuterManager(this, state, subject); + + Subscriber concatSubscriber = new Subscriber() { + @Override + public void onNext(T t) { + actualSubscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + actualSubscriber.onError(e); + } + + @Override + public void onCompleted() { + actualSubscriber.onCompleted(); + } + + @Override + public void setProducer(Producer p) { + outerProducer.setConcatProducer(p); + } + }; + + subject.onBackpressureBuffer().concatMap(new Func1, Observable>() { + @Override + public Observable call(Observable v) { + return v.onBackpressureBuffer(); + } + }).unsafeSubscribe(concatSubscriber); + + actualSubscriber.add(concatSubscriber); + actualSubscriber.add(outerProducer); + actualSubscriber.setProducer(outerProducer); + } - private static class AsyncOuterSubscriber extends ConcurrentLinkedQueueimplements Producer, Subscription, Observer> { - /** */ - private static final long serialVersionUID = -7884904861928856832L; + static final class AsyncOuterManager implements Producer, Subscription, Observer> { private volatile int isUnsubscribed; @SuppressWarnings("rawtypes") - private static final AtomicIntegerFieldUpdater IS_UNSUBSCRIBED = AtomicIntegerFieldUpdater.newUpdater(AsyncOuterSubscriber.class, "isUnsubscribed"); + private static final AtomicIntegerFieldUpdater IS_UNSUBSCRIBED = AtomicIntegerFieldUpdater.newUpdater(AsyncOuterManager.class, "isUnsubscribed"); private final AsyncOnSubscribe parent; private final SerializedObserver> serializedSubscriber; - private final Set subscriptions = new HashSet(); + private final CompositeSubscription subscriptions = new CompositeSubscription(); - private boolean hasTerminated = false; - private boolean onNextCalled = false; + private boolean hasTerminated; + private boolean onNextCalled; private S state; private final UnicastSubject> merger; - - public AsyncOuterSubscriber(AsyncOnSubscribe parent, S initialState, UnicastSubject> merger) { + + boolean emitting; + List requests; + Producer concatProducer; + + long expectedDelivery; + + public AsyncOuterManager(AsyncOnSubscribe parent, S initialState, UnicastSubject> merger) { this.parent = parent; this.serializedSubscriber = new SerializedObserver>(this); this.state = initialState; @@ -349,18 +380,25 @@ public AsyncOuterSubscriber(AsyncOnSubscribe parent, S initialState, Unica @Override public void unsubscribe() { if (IS_UNSUBSCRIBED.compareAndSet(this, 0, 1)) { - // it's safe to process terminal behavior - if (isEmpty()) { - parent.onUnsubscribe(state); - } - for (Subscription s : subscriptions) { - if (!s.isUnsubscribed()) { - s.unsubscribe(); + synchronized (this) { + if (emitting) { + requests = new ArrayList(); + requests.add(0L); + return; } + emitting = true; } + cleanup(); } } + void setConcatProducer(Producer p) { + if (concatProducer != null) { + throw new IllegalStateException("setConcatProducer may be called at most once!"); + } + concatProducer = p; + } + @Override public boolean isUnsubscribed() { return isUnsubscribed != 0; @@ -369,47 +407,149 @@ public boolean isUnsubscribed() { public void nextIteration(long requestCount) { state = parent.next(state, requestCount, serializedSubscriber); } + + void cleanup() { + subscriptions.unsubscribe(); + try { + parent.onUnsubscribe(state); + } catch (Throwable ex) { + handleThrownError(ex); + } + } @Override public void request(long n) { - int size = 0; - Long r; + if (n == 0) { + return; + } + if (n < 0) { + throw new IllegalStateException("Request can't be negative! " + n); + } + boolean quit = false; synchronized (this) { - size = size(); - add(n); - r = n; + if (emitting) { + List q = requests; + if (q == null) { + q = new ArrayList(); + requests = q; + } + q.add(n); + + quit = true; + } else { + emitting = true; + } + } + + concatProducer.request(n); + + if (quit) { + return; + } + + if (tryEmit(n)) { + return; } - if (size == 0) { - do { - // check if unsubscribed before doing any work - if (isUnsubscribed()) { - unsubscribe(); + for (;;) { + List q; + synchronized (this) { + q = requests; + if (q == null) { + emitting = false; return; } - // otherwise try one iteration for a request of `numRequested` elements - try { - onNextCalled = false; - nextIteration(r); - if (onNextCalled) - r = poll(); - if (hasTerminated || isUnsubscribed()) { - parent.onUnsubscribe(state); - } - } catch (Throwable ex) { - handleThrownError(parent, state, ex); + requests = null; + } + + for (long r : q) { + if (tryEmit(r)) { return; } - } while (r != null && !hasTerminated); + } } } - private void handleThrownError(final AsyncOnSubscribe p, S st, Throwable ex) { + /** + * Called when a source has produced less than its provision (completed prematurely); this will trigger the generation of another + * source that will hopefully emit the missing amount. + * @param n the missing amount to produce via a new source. + */ + public void requestRemaining(long n) { + if (n == 0) { + return; + } + if (n < 0) { + throw new IllegalStateException("Request can't be negative! " + n); + } + synchronized (this) { + if (emitting) { + List q = requests; + if (q == null) { + q = new ArrayList(); + requests = q; + } + q.add(n); + + return; + } + emitting = true; + } + + if (tryEmit(n)) { + return; + } + for (;;) { + List q; + synchronized (this) { + q = requests; + if (q == null) { + emitting = false; + return; + } + requests = null; + } + + for (long r : q) { + if (tryEmit(r)) { + return; + } + } + } + } + + boolean tryEmit(long n) { + if (isUnsubscribed()) { + cleanup(); + return true; + } + + try { + onNextCalled = false; + expectedDelivery = n; + nextIteration(n); + + if (hasTerminated || isUnsubscribed()) { + cleanup(); + return true; + } + if (!onNextCalled) { + handleThrownError(new IllegalStateException("No events emitted!")); + return true; + } + } catch (Throwable ex) { + handleThrownError(ex); + return true; + } + return false; + } + + private void handleThrownError(Throwable ex) { if (hasTerminated) { RxJavaPlugins.getInstance().getErrorHandler().handleError(ex); } else { hasTerminated = true; merger.onError(ex); - unsubscribe(); + cleanup(); } } @@ -431,10 +571,6 @@ public void onError(Throwable e) { merger.onError(e); } - // This exists simply to check if the subscription has already been - // terminated before getting access to the subscription - private static Subscription SUBSCRIPTION_SENTINEL = new BooleanSubscription(); - @Override public void onNext(final Observable t) { if (onNextCalled) { @@ -447,27 +583,43 @@ public void onNext(final Observable t) { } private void subscribeBufferToObservable(final Observable t) { - BufferUntilSubscriber buffer = BufferUntilSubscriber. create(); - final AtomicReference holder = new AtomicReference(null); - Subscription innerSubscription = t - .doOnTerminate(new Action0() { + final BufferUntilSubscriber buffer = BufferUntilSubscriber. create(); + + final long expected = expectedDelivery; + final Subscriber s = new Subscriber() { + long remaining = expected; + @Override + public void onNext(T t) { + remaining--; + buffer.onNext(t); + } + @Override + public void onError(Throwable e) { + buffer.onError(e); + } + @Override + public void onCompleted() { + buffer.onCompleted(); + long r = remaining; + if (r > 0) { + requestRemaining(r); + } + } + }; + subscriptions.add(s); + + t.doOnTerminate(new Action0() { @Override public void call() { - if (!holder.compareAndSet(null, SUBSCRIPTION_SENTINEL)) { - Subscription h = holder.get(); - subscriptions.remove(h); - } + subscriptions.remove(s); }}) - .subscribe(buffer); + .subscribe(s); - if (holder.compareAndSet(null, innerSubscription)) { - subscriptions.add(innerSubscription); - } merger.onNext(buffer); } } - private static final class UnicastSubject extends Observableimplements Observer { + static final class UnicastSubject extends Observableimplements Observer { public static UnicastSubject create() { return new UnicastSubject(new State()); } @@ -475,16 +627,7 @@ public static UnicastSubject create() { private State state; protected UnicastSubject(final State state) { - super(new OnSubscribe() { - @Override - public void call(Subscriber s) { - if (state.subscriber != null) { - s.onError(new IllegalStateException("There can be only one subscriber")); - } else { - state.subscriber = s; - } - } - }); + super(state); this.state = state; } @@ -503,8 +646,18 @@ public void onNext(T t) { state.subscriber.onNext(t); } - private static class State { + static final class State implements OnSubscribe { private Subscriber subscriber; + @Override + public void call(Subscriber s) { + synchronized (this) { + if (subscriber == null) { + subscriber = s; + return; + } + } + s.onError(new IllegalStateException("There can be only one subscriber")); + } } } } diff --git a/src/test/java/rx/observables/AsyncOnSubscribeTest.java b/src/test/java/rx/observables/AsyncOnSubscribeTest.java index 92537dc455..633d229921 100644 --- a/src/test/java/rx/observables/AsyncOnSubscribeTest.java +++ b/src/test/java/rx/observables/AsyncOnSubscribeTest.java @@ -38,7 +38,7 @@ public class AsyncOnSubscribeTest { @Before public void setup() { - subscriber = new TestSubscriber(o); + subscriber = new TestSubscriber(o, 0L); } @Test @@ -68,14 +68,20 @@ else if (state == 2) { // initial request emits [[1, 2, 3, 4]] on delay Observable.create(os).subscribe(subscriber); // next request emits [[5, 6, 7, 8]] firing immediately - subscriber.requestMore(2); + subscriber.requestMore(2); // triggers delayed observable scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + subscriber.assertNoErrors(); + subscriber.assertValues(1, 2); // final request completes subscriber.requestMore(3); - subscriber.awaitTerminalEventAndUnsubscribeOnTimeout(100, TimeUnit.MILLISECONDS); subscriber.assertNoErrors(); - subscriber.assertReceivedOnNext(Arrays.asList(new Integer[] {1, 2, 3, 4, 5, 6, 7, 8})); + subscriber.assertValues(1, 2, 3, 4, 5); + + subscriber.requestMore(3); + + subscriber.assertNoErrors(); + subscriber.assertValues(1, 2, 3, 4, 5, 6, 7, 8); subscriber.assertCompleted(); } @@ -89,6 +95,7 @@ public void call(Long requested, Observer> observe observer.onNext(Observable.range(1, requested.intValue())); }}); Observable.create(os).observeOn(scheduler).subscribe(subscriber); + subscriber.requestMore(RxRingBuffer.SIZE); scheduler.advanceTimeBy(10, TimeUnit.DAYS); subscriber.assertNoErrors(); subscriber.assertValueCount(RxRingBuffer.SIZE); @@ -118,7 +125,8 @@ public Integer call(Integer state, Long requested, Observer> observe observer.onCompleted(); }}); Observable.create(os).subscribe(subscriber); + subscriber.requestMore(1); subscriber.assertNoErrors(); subscriber.assertCompleted(); subscriber.assertNoValues(); @@ -150,6 +159,7 @@ public void call(Long requested, Observer> observe } }); Observable.create(os).subscribe(subscriber); + subscriber.requestMore(1); subscriber.assertError(IllegalStateException.class); subscriber.assertNotCompleted(); subscriber.assertReceivedOnNext(Arrays.asList(new Integer[] {1})); @@ -164,6 +174,7 @@ public void call(Long requested, Observer> observe throw new TestException(); }}); Observable.create(os).subscribe(subscriber); + subscriber.requestMore(1); subscriber.assertError(TestException.class); subscriber.assertNotCompleted(); subscriber.assertNoValues(); @@ -183,6 +194,7 @@ public Integer call(Integer state, Long requested, Observer> observer) { switch (state) { case 1: - observer.onNext(Observable.just(1) + observer.onNext(Observable.range(1, requested.intValue()) .subscribeOn(scheduler) .doOnUnsubscribe(new Action0(){ @Override @@ -383,8 +401,11 @@ public void call() { .subscribe(subscriber); sub.set(subscription); subscriber.assertNoValues(); + subscriber.requestMore(1); + scheduler.triggerActions(); + subscriber.requestMore(1); scheduler.triggerActions(); - subscriber.assertValue(1); + subscriber.assertValueCount(2); subscriber.assertNotCompleted(); subscriber.assertNoErrors(); assertEquals("did not unsub from 1st observable after terminal", 1, l1.get()); @@ -405,4 +426,43 @@ public void call(Long state, Observer> observer) { observer.onNext(Observable.just(new Bar())); }}); } + + @Test + public void testUnderdeliveryCorrection() { + OnSubscribe os = AsyncOnSubscribe.createStateful(new Func0(){ + @Override + public Integer call() { + return 1; + }}, + new Func3>, Integer>(){ + @Override + public Integer call(Integer state, Long requested, Observer> observer) { + switch (state) { + case 1: + observer.onNext(Observable.just(1)); + break; + default: + observer.onNext(Observable.range(1, requested.intValue())); + break; + } + return state + 1; + }}); + Observable.create(os).subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertNotCompleted(); + subscriber.assertNoValues(); + + subscriber.requestMore(2); + + subscriber.assertNoErrors(); + subscriber.assertValueCount(2); + + subscriber.requestMore(5); + + subscriber.assertNoErrors(); + subscriber.assertValueCount(7); + + subscriber.assertNotCompleted(); + } } From 82c13b0a9d1b3634042abd367b561ba567a6243f Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sun, 20 Sep 2015 03:14:29 +0300 Subject: [PATCH 387/857] Safer error handling in BlockingOperatorToFuture --- .../rx/internal/operators/BlockingOperatorToFuture.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java b/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java index ee9a5fe314..29021405ca 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorToFuture.java @@ -118,8 +118,10 @@ public T get(long timeout, TimeUnit unit) throws InterruptedException, Execution } private T getValue() throws ExecutionException { - if (error.get() != null) { - throw new ExecutionException("Observable onError", error.get()); + final Throwable throwable = error.get(); + + if (throwable != null) { + throw new ExecutionException("Observable onError", throwable); } else if (cancelled) { // Contract of Future.get() requires us to throw this: throw new CancellationException("Subscription unsubscribed"); From 1b31347605a5a8b326983e1ad47faf1aa7711e4d Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sun, 20 Sep 2015 03:25:11 +0300 Subject: [PATCH 388/857] Fix synchronization on non-final field in BufferUntilSubscriber --- src/main/java/rx/internal/operators/BufferUntilSubscriber.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/BufferUntilSubscriber.java b/src/main/java/rx/internal/operators/BufferUntilSubscriber.java index 737b8d2bee..e4722c9a60 100644 --- a/src/main/java/rx/internal/operators/BufferUntilSubscriber.java +++ b/src/main/java/rx/internal/operators/BufferUntilSubscriber.java @@ -70,7 +70,7 @@ boolean casObserverRef(Observer expected, Observer next) return OBSERVER_UPDATER.compareAndSet(this, expected, next); } - Object guard = new Object(); + final Object guard = new Object(); /* protected by guard */ boolean emitting = false; From f178fb3a73996ed29deb7e92711492c93578de53 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sun, 20 Sep 2015 03:59:43 +0300 Subject: [PATCH 389/857] Remove unused private method from CachedObservable and make "state" final --- .../rx/internal/operators/CachedObservable.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/internal/operators/CachedObservable.java b/src/main/java/rx/internal/operators/CachedObservable.java index 0231c3590f..1995174eff 100644 --- a/src/main/java/rx/internal/operators/CachedObservable.java +++ b/src/main/java/rx/internal/operators/CachedObservable.java @@ -29,8 +29,9 @@ * @param the source element type */ public final class CachedObservable extends Observable { + /** The cache and replay state. */ - private CacheState state; + private final CacheState state; /** * Creates a cached Observable with a default capacity hint of 16. @@ -82,15 +83,7 @@ private CachedObservable(OnSubscribe onSubscribe, CacheState state) { /* public */ boolean hasObservers() { return state.producers.length != 0; } - - /** - * Returns the number of events currently cached. - * @return - */ - /* public */ int cachedEventCount() { - return state.size(); - } - + /** * Contains the active child producers and the values to replay. * From 9b10529f6f72eae351c5a14013643bc3d88c123e Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sun, 20 Sep 2015 04:23:37 +0300 Subject: [PATCH 390/857] Make field final and remove unnecessary unboxing in OnSubscribeRedo.RetryWithPredicate --- src/main/java/rx/internal/operators/OnSubscribeRedo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 1431d4581c..48521d00b1 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -98,7 +98,7 @@ public Notification call(Notification terminalNotification) { } public static final class RetryWithPredicate implements Func1>, Observable>> { - private Func2 predicate; + private final Func2 predicate; public RetryWithPredicate(Func2 predicate) { this.predicate = predicate; @@ -111,7 +111,7 @@ public Observable> call(Observable call(Notification n, Notification term) { final int value = n.getValue(); - if (predicate.call(value, term.getThrowable()).booleanValue()) + if (predicate.call(value, term.getThrowable())) return Notification.createOnNext(value + 1); else return (Notification) term; From 27bb157a45275a57d8efd073e010ea09e8958ba6 Mon Sep 17 00:00:00 2001 From: Kevin Coughlin Date: Sun, 20 Sep 2015 23:20:03 -0400 Subject: [PATCH 391/857] Lint fixes for unnecessary unboxing. --- src/main/java/rx/internal/operators/OnSubscribeRedo.java | 2 +- src/main/java/rx/internal/operators/OperatorReplay.java | 2 +- src/main/java/rx/schedulers/TestScheduler.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 1431d4581c..cf517cb90e 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -111,7 +111,7 @@ public Observable> call(Observable call(Notification n, Notification term) { final int value = n.getValue(); - if (predicate.call(value, term.getThrowable()).booleanValue()) + if (predicate.call(value, term.getThrowable())) return Notification.createOnNext(value + 1); else return (Notification) term; diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index 6b42f1fb51..93d78ee14b 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -805,7 +805,7 @@ public void replay(InnerProducer output) { int sourceIndex = size; Integer destIndexObject = output.index(); - int destIndex = destIndexObject != null ? destIndexObject.intValue() : 0; + int destIndex = destIndexObject != null ? destIndexObject : 0; long r = output.get(); long r0 = r; diff --git a/src/main/java/rx/schedulers/TestScheduler.java b/src/main/java/rx/schedulers/TestScheduler.java index 358581ab74..fb7c0ef861 100644 --- a/src/main/java/rx/schedulers/TestScheduler.java +++ b/src/main/java/rx/schedulers/TestScheduler.java @@ -57,9 +57,9 @@ private static class CompareActionsByTime implements Comparator { @Override public int compare(TimedAction action1, TimedAction action2) { if (action1.time == action2.time) { - return Long.valueOf(action1.count).compareTo(Long.valueOf(action2.count)); + return Long.valueOf(action1.count).compareTo(action2.count); } else { - return Long.valueOf(action1.time).compareTo(Long.valueOf(action2.time)); + return Long.valueOf(action1.time).compareTo(action2.time); } } } From 5e90dd7e524052601ba3af358569da435c834f80 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Tue, 22 Sep 2015 01:39:46 +0300 Subject: [PATCH 392/857] Remove unused field updater from SubjectSubscriptionManager --- src/main/java/rx/subjects/SubjectSubscriptionManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/rx/subjects/SubjectSubscriptionManager.java b/src/main/java/rx/subjects/SubjectSubscriptionManager.java index a160c720a3..542d050c39 100644 --- a/src/main/java/rx/subjects/SubjectSubscriptionManager.java +++ b/src/main/java/rx/subjects/SubjectSubscriptionManager.java @@ -40,8 +40,6 @@ = AtomicReferenceFieldUpdater.newUpdater(SubjectSubscriptionManager.class, State.class, "state"); /** Stores the latest value or the terminal value for some Subjects. */ volatile Object latest; - static final AtomicReferenceFieldUpdater LATEST_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(SubjectSubscriptionManager.class, Object.class, "latest"); /** Indicates that the subject is active (cheaper than checking the state).*/ boolean active = true; /** Action called when a new subscriber subscribes but before it is added to the state. */ From ab02902665fdac2c77c3e844b4660072e4c603c6 Mon Sep 17 00:00:00 2001 From: Kevin Coughlin Date: Mon, 21 Sep 2015 19:02:23 -0400 Subject: [PATCH 393/857] Use ternary for comparison in place of Long.compareTo for Java 6 support. --- src/main/java/rx/schedulers/TestScheduler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/schedulers/TestScheduler.java b/src/main/java/rx/schedulers/TestScheduler.java index fb7c0ef861..c808a1a366 100644 --- a/src/main/java/rx/schedulers/TestScheduler.java +++ b/src/main/java/rx/schedulers/TestScheduler.java @@ -57,9 +57,9 @@ private static class CompareActionsByTime implements Comparator { @Override public int compare(TimedAction action1, TimedAction action2) { if (action1.time == action2.time) { - return Long.valueOf(action1.count).compareTo(action2.count); + return action1.count < action2.count ? -1 : ((action1.count > action2.count) ? 1 : 0); } else { - return Long.valueOf(action1.time).compareTo(action2.time); + return action1.time < action2.time ? -1 : ((action1.time > action2.time) ? 1 : 0); } } } From e16300760bcbb6847c20dc1674bee8ba06793bc8 Mon Sep 17 00:00:00 2001 From: Steve Gury Date: Mon, 28 Sep 2015 14:27:48 -0700 Subject: [PATCH 394/857] Fix typo in a comment inside Observable.subscribe sigificent -> significant alreay -> already --- src/main/java/rx/Observable.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 5d6d77c15f..0b50ef9268 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7821,7 +7821,8 @@ private static Subscription subscribe(Subscriber subscriber, Obse 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(observable, observable.onSubscribe).call(subscriber); From 53ff79959c709a09bdefc74d555f734d21f57ec2 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 29 Sep 2015 09:53:19 +0200 Subject: [PATCH 395/857] Fix for take() reentrancy bug. --- .../rx/internal/operators/OperatorTake.java | 4 ++-- .../internal/operators/OperatorTakeTest.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorTake.java b/src/main/java/rx/internal/operators/OperatorTake.java index 31811537b5..d1cc1cbd09 100644 --- a/src/main/java/rx/internal/operators/OperatorTake.java +++ b/src/main/java/rx/internal/operators/OperatorTake.java @@ -68,8 +68,8 @@ public void onError(Throwable e) { @Override public void onNext(T i) { - if (!isUnsubscribed()) { - boolean stop = ++count >= limit; + if (!isUnsubscribed() && count++ < limit) { + boolean stop = count == limit; child.onNext(i); if (stop && !completed) { completed = true; diff --git a/src/test/java/rx/internal/operators/OperatorTakeTest.java b/src/test/java/rx/internal/operators/OperatorTakeTest.java index 3384445d5b..4173f08892 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeTest.java @@ -32,6 +32,7 @@ import rx.functions.*; import rx.observers.*; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorTakeTest { @@ -417,4 +418,24 @@ public void onNext(Integer t) { ts.assertError(TestException.class); ts.assertNotCompleted(); } + + @Test + public void testReentrantTake() { + final PublishSubject source = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.take(1).doOnNext(new Action1() { + @Override + public void call(Integer v) { + source.onNext(2); + } + }).subscribe(ts); + + source.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertCompleted(); + } } From 566ee93e1e785a574c55a03a839b8f9b96a32cb0 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 30 Sep 2015 09:04:08 +0200 Subject: [PATCH 396/857] Hiding start(), moved test to compensate. --- src/main/java/rx/schedulers/Schedulers.java | 2 +- .../rx/{internal => }/schedulers/SchedulerLifecycleTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename src/test/java/rx/{internal => }/schedulers/SchedulerLifecycleTest.java (97%) diff --git a/src/main/java/rx/schedulers/Schedulers.java b/src/main/java/rx/schedulers/Schedulers.java index 2376f0fa8a..7dd8186616 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 static void start() { + /* public testonly */ static void start() { Schedulers s = INSTANCE; synchronized (s) { if (s.computationScheduler instanceof SchedulerLifecycle) { diff --git a/src/test/java/rx/internal/schedulers/SchedulerLifecycleTest.java b/src/test/java/rx/schedulers/SchedulerLifecycleTest.java similarity index 97% rename from src/test/java/rx/internal/schedulers/SchedulerLifecycleTest.java rename to src/test/java/rx/schedulers/SchedulerLifecycleTest.java index 1d6b1b6abc..ce6d743cad 100644 --- a/src/test/java/rx/internal/schedulers/SchedulerLifecycleTest.java +++ b/src/test/java/rx/schedulers/SchedulerLifecycleTest.java @@ -1,4 +1,4 @@ -package rx.internal.schedulers; +package rx.schedulers; import static org.junit.Assert.*; @@ -9,6 +9,7 @@ import rx.Scheduler.Worker; import rx.functions.Action0; +import rx.internal.schedulers.GenericScheduledExecutorService; import rx.internal.util.RxRingBuffer; import rx.schedulers.Schedulers; import rx.subscriptions.CompositeSubscription; From 89390e3db2e391f40860ba82630532af584eed72 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 2 Oct 2015 09:06:46 +0200 Subject: [PATCH 397/857] DoOnEach: report both original exception and callback exception. This came up in a [Stackoverflow](http://stackoverflow.com/questions/32889008/do-operators-instead-of-a-whole-subscriber) answer. If the doOnError's callback or the doOnEach's onError method throws, any non-fatal exception replaced the original error which got lost. This PR will wrap them both into a CompositeException. 2.x note: since Java 8 supports `addSuppressed` all callbacks in this situation either attach to the original exception or the original exception is attached to the callback's exception. --- src/main/java/rx/Observable.java | 7 +++ .../internal/operators/OperatorDoOnEach.java | 7 ++- .../operators/OperatorDoOnEachTest.java | 48 +++++++++++++------ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 0b50ef9268..d9121c8619 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4362,6 +4362,10 @@ public final void onNext(T v) { /** * Modifies the source Observable so that it notifies an Observer for each item it emits. *

+ * In case the onError of the supplied observer throws, the downstream will receive a composite exception containing + * the original exception and the exception thrown by onError. If the onNext or the onCompleted methods + * of the supplied observer throws, the downstream will be terminated and wil receive this thrown exception. + *

* *

*
Scheduler:
@@ -4380,6 +4384,9 @@ public final Observable doOnEach(Observer observer) { /** * Modifies the source Observable so that it invokes an action if it calls {@code onError}. *

+ * In case the onError action throws, the downstream will receive a composite exception containing + * the original exception and the exception thrown by onError. + *

* *

*
Scheduler:
diff --git a/src/main/java/rx/internal/operators/OperatorDoOnEach.java b/src/main/java/rx/internal/operators/OperatorDoOnEach.java index 4b3e8d54cf..1e3a680dac 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnEach.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnEach.java @@ -15,9 +15,11 @@ */ package rx.internal.operators; +import java.util.Arrays; + import rx.*; import rx.Observable.Operator; -import rx.exceptions.Exceptions; +import rx.exceptions.*; /** * Converts the elements of an observable sequence to the specified type. @@ -62,7 +64,8 @@ public void onError(Throwable e) { try { doOnEachObserver.onError(e); } catch (Throwable e2) { - Exceptions.throwOrReport(e2, observer); + Exceptions.throwIfFatal(e2); + observer.onError(new CompositeException(Arrays.asList(e, e2))); return; } observer.onError(e); diff --git a/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java b/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java index 2ad9a36828..3c4cf9f9bb 100644 --- a/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java +++ b/src/test/java/rx/internal/operators/OperatorDoOnEachTest.java @@ -17,25 +17,19 @@ import static org.junit.Assert.*; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable; -import rx.Observer; -import rx.Subscriber; -import rx.exceptions.OnErrorNotImplementedException; -import rx.functions.Action1; -import rx.functions.Func1; +import static org.mockito.Mockito.*; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.*; +import org.mockito.*; + +import rx.*; +import rx.exceptions.*; +import rx.functions.*; +import rx.observers.TestSubscriber; + public class OperatorDoOnEachTest { @Mock @@ -201,4 +195,28 @@ public void call(Object o) { System.out.println("Received exception: " + e); } } + + @Test + public void testOnErrorThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.error(new TestException()) + .doOnError(new Action1() { + @Override + public void call(Throwable e) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(CompositeException.class); + + CompositeException ex = (CompositeException)ts.getOnErrorEvents().get(0); + + List exceptions = ex.getExceptions(); + assertEquals(2, exceptions.size()); + assertTrue(exceptions.get(0) instanceof TestException); + assertTrue(exceptions.get(1) instanceof TestException); + } } \ No newline at end of file From a239d0297bcef5339ec84c4a45d9e6a1b6410f6d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Sat, 3 Oct 2015 09:28:06 +0200 Subject: [PATCH 398/857] Eager concatMap --- src/main/java/rx/Observable.java | 406 ++++++++++++++++++ .../operators/OperatorEagerConcatMap.java | 308 +++++++++++++ .../util/atomic/SpscLinkedArrayQueue.java | 356 +++++++++++++++ .../operators/OperatorEagerConcatMapTest.java | 397 +++++++++++++++++ 4 files changed, 1467 insertions(+) create mode 100644 src/main/java/rx/internal/operators/OperatorEagerConcatMap.java create mode 100644 src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java create mode 100644 src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 5d6d77c15f..653e326276 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4546,6 +4546,412 @@ public final Observable doOnUnsubscribe(final Action0 unsubscribe) { return lift(new OperatorDoOnUnsubscribe(unsubscribe)); } + /** + * Concatenates up to 2 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager(Observable o1, Observable o2) { + return concatEager(Arrays.asList(o1, o2)); + } + + /** + * Concatenates up to 3 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @param o3 the third source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager( + Observable o1, Observable o2, + Observable o3 + ) { + return concatEager(Arrays.asList(o1, o2, o3)); + } + + /** + * Concatenates up to 4 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @param o3 the third source + * @param o4 the fourth source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager( + Observable o1, Observable o2, + Observable o3, Observable o4 + ) { + return concatEager(Arrays.asList(o1, o2, o3, o4)); + } + + /** + * Concatenates up to 5 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @param o3 the third source + * @param o4 the fourth source + * @param o5 the fifth source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager( + Observable o1, Observable o2, + Observable o3, Observable o4, + Observable o5 + ) { + return concatEager(Arrays.asList(o1, o2, o3, o4, o5)); + } + + /** + * Concatenates up to 6 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @param o3 the third source + * @param o4 the fourth source + * @param o5 the fifth source + * @param o6 the sixth source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager( + Observable o1, Observable o2, + Observable o3, Observable o4, + Observable o5, Observable o6 + ) { + return concatEager(Arrays.asList(o1, o2, o3, o4, o5, o6)); + } + + /** + * Concatenates up to 7 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @param o3 the third source + * @param o4 the fourth source + * @param o5 the fifth source + * @param o6 the sixth source + * @param o7 the seventh source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager( + Observable o1, Observable o2, + Observable o3, Observable o4, + Observable o5, Observable o6, + Observable o7 + ) { + return concatEager(Arrays.asList(o1, o2, o3, o4, o5, o6, o7)); + } + + /** + * Concatenates up to 8 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @param o3 the third source + * @param o4 the fourth source + * @param o5 the fifth source + * @param o6 the sixth source + * @param o7 the seventh source + * @param o8 the eight source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager( + Observable o1, Observable o2, + Observable o3, Observable o4, + Observable o5, Observable o6, + Observable o7, Observable o8 + ) { + return concatEager(Arrays.asList(o1, o2, o3, o4, o5, o6, o7, o8)); + } + + /** + * Concatenates up to 9 sources eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param o1 the first source + * @param o2 the second source + * @param o3 the third source + * @param o4 the fourth source + * @param o5 the fifth source + * @param o6 the sixth source + * @param o7 the seventh source + * @param o8 the eight source + * @param o9 the nine source + * @return + */ + @Experimental + @SuppressWarnings("unchecked") + public static Observable concatEager( + Observable o1, Observable o2, + Observable o3, Observable o4, + Observable o5, Observable o6, + Observable o7, Observable o8, + Observable o9 + ) { + return concatEager(Arrays.asList(o1, o2, o3, o4, o5, o6, o7, o8, o9)); + } + + /** + * Concatenates a sequence of Observables eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param sources a sequence of Observables that need to be eagerly concatenated + * @return + */ + @Experimental + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Observable concatEager(Iterable> sources) { + return Observable.from(sources).concatMapEager((Func1)UtilityFunctions.identity()); + } + + /** + * Concatenates a sequence of Observables eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param sources a sequence of Observables that need to be eagerly concatenated + * @param capacityHint hints about the number of expected source sequence values + * @return + */ + @Experimental + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Observable concatEager(Iterable> sources, int capacityHint) { + return Observable.from(sources).concatMapEager((Func1)UtilityFunctions.identity(), capacityHint); + } + + /** + * Concatenates an Observable sequence of Observables eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param sources a sequence of Observables that need to be eagerly concatenated + * @return + */ + @Experimental + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Observable concatEager(Observable> sources) { + return sources.concatMapEager((Func1)UtilityFunctions.identity()); + } + + /** + * Concatenates an Observable sequence of Observables eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values queued up in an unbounded buffer.
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * @param the value type + * @param sources a sequence of Observables that need to be eagerly concatenated + * @param capacityHint hints about the number of expected source sequence values + * @return + */ + @Experimental + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Observable concatEager(Observable> sources, int capacityHint) { + return sources.concatMapEager((Func1)UtilityFunctions.identity(), capacityHint); + } + + /** + * Maps a sequence of values into Observables and concatenates these Observables eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values 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 bec eagerly concatenated + * @return + */ + @Experimental + public final Observable concatMapEager(Func1> mapper) { + return concatMapEager(mapper, RxRingBuffer.SIZE); + } + + /** + * Maps a sequence of values into Observables and concatenates these Observables eagerly into a single stream of values. + * + *

+ * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and + * they are drained in order, one after the previous completes. + * + *

+ *
Backpressure:
+ *
Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources are subscribed to + * in unbounded mode and values 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 bec eagerly concatenated + * @param capacityHint hints about the number of expected source sequence values + * @return + */ + @Experimental + public final Observable concatMapEager(Func1> mapper, int capacityHint) { + if (capacityHint < 1) { + throw new IllegalArgumentException("capacityHint > 0 required but it was " + capacityHint); + } + return lift(new OperatorEagerConcatMap(mapper, capacityHint)); + } + /** * Returns an Observable that emits the single item at a specified index in a sequence of emissions from a * source Observbable. diff --git a/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java new file mode 100644 index 0000000000..127f2fbd51 --- /dev/null +++ b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java @@ -0,0 +1,308 @@ +/** + * 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.*; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.Observable; +import rx.Observable.Operator; +import rx.exceptions.Exceptions; +import rx.functions.*; +import rx.internal.util.unsafe.SpscArrayQueue; +import rx.subscriptions.Subscriptions; + +public final class OperatorEagerConcatMap implements Operator { + final Func1> mapper; + final int bufferSize; + public OperatorEagerConcatMap(Func1> mapper, int bufferSize) { + this.mapper = mapper; + this.bufferSize = bufferSize; + } + + @Override + public Subscriber call(Subscriber t) { + EagerOuterSubscriber outer = new EagerOuterSubscriber(mapper, bufferSize, t); + outer.init(); + return outer; + } + + static final class EagerOuterProducer extends AtomicLong implements Producer { + /** */ + private static final long serialVersionUID = -657299606803478389L; + + final EagerOuterSubscriber parent; + + public EagerOuterProducer(EagerOuterSubscriber parent) { + this.parent = parent; + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalStateException("n >= 0 required but it was " + n); + } + + if (n > 0) { + BackpressureUtils.getAndAddRequest(this, n); + parent.drain(); + } + } + } + + static final class EagerOuterSubscriber extends Subscriber { + final Func1> mapper; + final int bufferSize; + final Subscriber actual; + + final LinkedList> subscribers; + + volatile boolean done; + Throwable error; + + volatile boolean cancelled; + + final AtomicInteger wip; + private EagerOuterProducer sharedProducer; + + public EagerOuterSubscriber(Func1> mapper, int bufferSize, + Subscriber actual) { + this.mapper = mapper; + this.bufferSize = bufferSize; + this.actual = actual; + this.subscribers = new LinkedList>(); + this.wip = new AtomicInteger(); + } + + void init() { + sharedProducer = new EagerOuterProducer(this); + add(Subscriptions.create(new Action0() { + @Override + public void call() { + cancelled = true; + if (wip.getAndIncrement() == 0) { + cleanup(); + } + } + })); + actual.add(this); + actual.setProducer(sharedProducer); + } + + void cleanup() { + List list; + synchronized (subscribers) { + list = new ArrayList(subscribers); + subscribers.clear(); + } + + for (Subscription s : list) { + s.unsubscribe(); + } + } + + @Override + public void onNext(T t) { + Observable observable; + + try { + observable = mapper.call(t); + } catch (Throwable e) { + Exceptions.throwOrReport(e, actual, t); + return; + } + + EagerInnerSubscriber inner = new EagerInnerSubscriber(this, bufferSize); + if (cancelled) { + return; + } + synchronized (subscribers) { + if (cancelled) { + return; + } + subscribers.add(inner); + } + if (cancelled) { + return; + } + observable.unsafeSubscribe(inner); + drain(); + } + + @Override + public void onError(Throwable e) { + error = e; + done = true; + drain(); + } + + @Override + public void onCompleted() { + done = true; + drain(); + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + int missed = 1; + + final AtomicLong requested = sharedProducer; + final Subscriber actualSubscriber = this.actual; + + for (;;) { + + if (cancelled) { + cleanup(); + return; + } + + EagerInnerSubscriber innerSubscriber; + + boolean outerDone = done; + synchronized (subscribers) { + innerSubscriber = subscribers.peek(); + } + boolean empty = innerSubscriber == null; + + if (outerDone) { + Throwable error = this.error; + if (error != null) { + cleanup(); + actualSubscriber.onError(error); + return; + } else + if (empty) { + actualSubscriber.onCompleted(); + return; + } + } + + if (!empty) { + long requestedAmount = requested.get(); + long emittedAmount = 0L; + boolean unbounded = requestedAmount == Long.MAX_VALUE; + + Queue innerQueue = innerSubscriber.queue; + boolean innerDone = false; + + + for (;;) { + outerDone = innerSubscriber.done; + R v = innerQueue.peek(); + empty = v == null; + + if (outerDone) { + Throwable innerError = innerSubscriber.error; + if (innerError != null) { + cleanup(); + actualSubscriber.onError(innerError); + return; + } else + if (empty) { + synchronized (subscribers) { + subscribers.poll(); + } + innerSubscriber.unsubscribe(); + innerDone = true; + break; + } + } + + if (empty) { + break; + } + + if (requestedAmount == 0L) { + break; + } + + innerQueue.poll(); + + try { + actualSubscriber.onNext(v); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, actualSubscriber, v); + return; + } + + requestedAmount--; + emittedAmount--; + } + + if (emittedAmount != 0L) { + if (!unbounded) { + requested.addAndGet(emittedAmount); + } + if (!innerDone) { + innerSubscriber.requestMore(-emittedAmount); + } + } + + if (innerDone) { + continue; + } + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + return; + } + } + } + } + + static final class EagerInnerSubscriber extends Subscriber { + final EagerOuterSubscriber parent; + final Queue queue; + + volatile boolean done; + Throwable error; + + public EagerInnerSubscriber(EagerOuterSubscriber parent, int bufferSize) { + super(); + this.parent = parent; + this.queue = new SpscArrayQueue(bufferSize); + request(bufferSize); + } + + @Override + public void onNext(T t) { + queue.offer(t); + parent.drain(); + } + + @Override + public void onError(Throwable e) { + error = e; + done = true; + parent.drain(); + } + + @Override + public void onCompleted() { + done = true; + parent.drain(); + } + + void requestMore(long n) { + request(n); + } + } +} diff --git a/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java new file mode 100644 index 0000000000..5a00430b96 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java @@ -0,0 +1,356 @@ +/** + * 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.util.atomic; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import rx.internal.util.unsafe.Pow2; + + +/* + * The code was inspired by the similarly named JCTools class: + * https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic + */ + +/** + * A single-producer single-consumer array-backed queue which can allocate new arrays in case the consumer is slower + * than the producer. + */ +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 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"); + private static final Object HAS_NEXT = new Object(); + + public SpscLinkedArrayQueue(final int bufferSize) { + int p2capacity = Pow2.roundToPowerOfTwo(bufferSize); + int mask = p2capacity - 1; + AtomicReferenceArray buffer = new AtomicReferenceArray(p2capacity + 1); + producerBuffer = buffer; + producerMask = mask; + adjustLookAheadStep(p2capacity); + consumerBuffer = buffer; + consumerMask = mask; + producerLookAhead = mask - 1; // we know it's all empty to start with + soProducerIndex(0L); + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single producer thread use only. + */ + @Override + public final boolean offer(final T e) { + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = producerBuffer; + final long index = lpProducerIndex(); + final int mask = producerMask; + final int offset = calcWrappedOffset(index, mask); + if (index < producerLookAhead) { + return writeToQueue(buffer, e, index, offset); + } else { + final int lookAheadStep = producerLookAheadStep; + // go around the buffer or resize if full (unless we hit max capacity) + int lookAheadElementOffset = calcWrappedOffset(index + lookAheadStep, mask); + if (null == lvElement(buffer, lookAheadElementOffset)) {// LoadLoad + producerLookAhead = index + lookAheadStep - 1; // joy, there's plenty of room + return writeToQueue(buffer, e, index, offset); + } else if (null == lvElement(buffer, calcWrappedOffset(index + 1, mask))) { // buffer is not full + return writeToQueue(buffer, e, index, offset); + } else { + resize(buffer, index, offset, e, mask); // add a buffer and link old to new + return true; + } + } + } + + private boolean writeToQueue(final AtomicReferenceArray buffer, final T e, final long index, final int offset) { + soProducerIndex(index + 1);// this ensures atomic write of long on 32bit platforms + soElement(buffer, offset, e);// StoreStore + return true; + } + + private void resize(final AtomicReferenceArray oldBuffer, final long currIndex, final int offset, final T e, + final long mask) { + final int capacity = oldBuffer.length(); + final AtomicReferenceArray newBuffer = new AtomicReferenceArray(capacity); + producerBuffer = newBuffer; + producerLookAhead = currIndex + mask - 1; + soProducerIndex(currIndex + 1);// this ensures correctness on 32bit platforms + soElement(newBuffer, offset, e);// StoreStore + soNext(oldBuffer, newBuffer); + soElement(oldBuffer, offset, HAS_NEXT); // new buffer is visible after element is + // inserted + } + + private void soNext(AtomicReferenceArray curr, AtomicReferenceArray next) { + soElement(curr, calcDirectOffset(curr.length() - 1), next); + } + @SuppressWarnings("unchecked") + private AtomicReferenceArray lvNext(AtomicReferenceArray curr) { + return (AtomicReferenceArray)lvElement(curr, calcDirectOffset(curr.length() - 1)); + } + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final T poll() { + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + boolean isNextBuffer = e == HAS_NEXT; + if (null != e && !isNextBuffer) { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(buffer, offset, null);// StoreStore + return (T) e; + } else if (isNextBuffer) { + return newBufferPoll(lvNext(buffer), index, mask); + } + + return null; + } + + @SuppressWarnings("unchecked") + private T newBufferPoll(AtomicReferenceArray nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + final T n = (T) lvElement(nextBuffer, offsetInNew);// LoadLoad + if (null == n) { + return null; + } else { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(nextBuffer, offsetInNew, null);// StoreStore + return n; + } + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final T peek() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + if (e == HAS_NEXT) { + return newBufferPeek(lvNext(buffer), index, mask); + } + + return (T) e; + } + + @Override + public void clear() { + while (poll() != null || !isEmpty()); + } + + @SuppressWarnings("unchecked") + private T newBufferPeek(AtomicReferenceArray nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + return (T) lvElement(nextBuffer, offsetInNew);// LoadLoad + } + + @Override + public final int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + @Override + public boolean isEmpty() { + return lvProducerIndex() == lvConsumerIndex(); + } + + private void adjustLookAheadStep(int capacity) { + producerLookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + private long lvProducerIndex() { + return producerIndex; + } + + private long lvConsumerIndex() { + return consumerIndex; + } + + private long lpProducerIndex() { + return producerIndex; + } + + private long lpConsumerIndex() { + return consumerIndex; + } + + private void soProducerIndex(long v) { + PRODUCER_INDEX.lazySet(this, v); + } + + private void soConsumerIndex(long v) { + CONSUMER_INDEX.lazySet(this, v); + } + + private static final int calcWrappedOffset(long index, int mask) { + return calcDirectOffset((int)index & mask); + } + private static final int calcDirectOffset(int index) { + return index; + } + private static final void soElement(AtomicReferenceArray buffer, int offset, Object e) { + buffer.lazySet(offset, e); + } + + private static final Object lvElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); + } + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public E[] toArray(E[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(T e) { + throw new UnsupportedOperationException(); + } + + @Override + public T remove() { + throw new UnsupportedOperationException(); + } + + @Override + public T element() { + throw new UnsupportedOperationException(); + } + + /** + * Offer two elements at the same time. + *

Don't use the regular offer() with this at all! + * @param first + * @param second + * @return + */ + public boolean offer(T first, T second) { + final AtomicReferenceArray buffer = producerBuffer; + final long p = producerIndex; + final int m = producerMask; + + int pi = calcWrappedOffset(p + 2, m); + + if (null == lvElement(buffer, pi)) { + pi = calcWrappedOffset(p, m); + soElement(buffer, pi + 1, second); + soProducerIndex(p + 2); + soElement(buffer, pi, first); + } else { + final int capacity = buffer.length(); + final AtomicReferenceArray newBuffer = new AtomicReferenceArray(capacity); + producerBuffer = newBuffer; + + pi = calcWrappedOffset(p, m); + soElement(newBuffer, pi + 1, second);// StoreStore + soElement(newBuffer, pi, first); + soNext(buffer, newBuffer); + + soProducerIndex(p + 2);// this ensures correctness on 32bit platforms + + soElement(buffer, pi, HAS_NEXT); // new buffer is visible after element is + } + + return true; + } +} + diff --git a/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java b/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java new file mode 100644 index 0000000000..8c7bd3d9e4 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java @@ -0,0 +1,397 @@ +/** + * 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.atomic.*; + +import org.junit.*; + +import rx.Observable; +import rx.exceptions.TestException; +import rx.functions.*; +import rx.internal.util.RxRingBuffer; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; + +public class OperatorEagerConcatMapTest { + TestSubscriber ts; + TestSubscriber tsBp; + + Func1> toJust = new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(t); + } + }; + + Func1> toRange = new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.range(t, 2); + } + }; + + @Before + public void before() { + ts = new TestSubscriber(); + tsBp = new TestSubscriber(0L); + } + + @Test + public void testSimple() { + Observable.range(1, 100).concatMapEager(toJust).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(100); + ts.assertCompleted(); + } + + @Test + public void testSimple2() { + Observable.range(1, 100).concatMapEager(toRange).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(200); + ts.assertCompleted(); + } + + @Test + public void testEagerness2() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source).subscribe(tsBp); + + Assert.assertEquals(2, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testEagerness3() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source, source).subscribe(tsBp); + + Assert.assertEquals(3, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testEagerness4() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(4, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testEagerness5() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(5, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testEagerness6() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(6, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testEagerness7() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(7, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testEagerness8() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source, source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(8, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testEagerness9() { + final AtomicInteger count = new AtomicInteger(); + Observable source = Observable.just(1).doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + + Observable.concatEager(source, source, source, source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(9, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + tsBp.assertNoValues(); + + tsBp.requestMore(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testMainError() { + Observable.error(new TestException()).concatMapEager(toJust).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void testInnerError() { + Observable.concatEager(Observable.just(1), Observable.error(new TestException())).subscribe(ts); + + ts.assertValue(1); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void testInnerEmpty() { + Observable.concatEager(Observable.empty(), Observable.empty()).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testMapperThrows() { + Observable.just(1).concatMapEager(new Func1>() { + @Override + public Observable call(Integer t) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidCapacityHint() { + Observable.just(1).concatMapEager(toJust, 0); + } + + @Test + public void testBackpressure() { + Observable.concatEager(Observable.just(1), Observable.just(1)).subscribe(tsBp); + + tsBp.assertNoErrors(); + tsBp.assertNoValues(); + tsBp.assertNotCompleted(); + + tsBp.requestMore(1); + tsBp.assertValue(1); + tsBp.assertNoErrors(); + tsBp.assertNotCompleted(); + + tsBp.requestMore(1); + tsBp.assertValues(1, 1); + tsBp.assertNoErrors(); + tsBp.assertCompleted(); + } + + @Test + public void testAsynchronousRun() { + Observable.range(1, 2).concatMapEager(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.range(1, 1000).subscribeOn(Schedulers.computation()); + } + }).observeOn(Schedulers.newThread()).subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoErrors(); + ts.assertValueCount(2000); + } + + @Test + public void testReentrantWork() { + final PublishSubject subject = PublishSubject.create(); + + final AtomicBoolean once = new AtomicBoolean(); + + subject.concatMapEager(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(t); + } + }) + .doOnNext(new Action1() { + @Override + public void call(Integer t) { + if (once.compareAndSet(false, true)) { + subject.onNext(2); + } + } + }) + .subscribe(ts); + + subject.onNext(1); + + ts.assertNoErrors(); + ts.assertNotCompleted(); + ts.assertValues(1, 2); + } + + @Test + public void testPrefetchIsBounded() { + final AtomicInteger count = new AtomicInteger(); + + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).concatMapEager(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.range(1, RxRingBuffer.SIZE * 2) + .doOnNext(new Action1() { + @Override + public void call(Integer t) { + count.getAndIncrement(); + } + }); + } + }).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotCompleted(); + Assert.assertEquals(RxRingBuffer.SIZE, count.get()); + } +} From 2f5358ad87da3c08ed477a237dddcb6708d2f341 Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Tue, 29 Sep 2015 10:25:12 -0700 Subject: [PATCH 399/857] Added warning to `Observable.doOnRequest` javadoc. --- src/main/java/rx/Observable.java | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 0b50ef9268..a1f14d789b 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4444,20 +4444,26 @@ public final void onNext(T args) { return lift(new OperatorDoOnEach(observer)); } - + /** - * Modifies the source {@code Observable} so that it invokes the given action when it receives a request for - * more items. + * Modifies the source {@code Observable} so that it invokes the given action when it receives a + * request for more items. + *

+ * Note: This operator is for tracing the internal behavior of back-pressure request + * patterns and generally intended for debugging use. *

- *
Scheduler:
- *
{@code doOnRequest} does not operate by default on a particular {@link Scheduler}.
+ *
Scheduler:
+ *
{@code doOnRequest} does not operate by default on a particular {@link Scheduler}.
*
* * @param onRequest - * the action that gets called when an observer requests items from this {@code Observable} + * the action that gets called when an observer requests items from this + * {@code Observable} * @return the source {@code Observable} modified so as to call this Action when appropriate - * @see ReactiveX operators documentation: Do - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @see ReactiveX operators + * documentation: Do + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical + * with the release number) */ @Beta public final Observable doOnRequest(final Action1 onRequest) { From 0167e0e910928f28fea465b165efa4a04ee822b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Karnok?= Date: Thu, 1 Oct 2015 20:40:02 +0200 Subject: [PATCH 400/857] pull back the Experimental/Beta of the changes until 1.1.x (+1 squashed commit) Squashed commits: [c6e43fc] 1.0.15. Beta/Deprecation of Subject state peeking methods. This should give users one release to prepare for the class structure changes. --- src/main/java/rx/subjects/AsyncSubject.java | 1 + src/main/java/rx/subjects/PublishSubject.java | 21 +++++++++++ src/main/java/rx/subjects/ReplaySubject.java | 2 ++ .../java/rx/subjects/SerializedSubject.java | 36 +++++++++++++++++++ src/main/java/rx/subjects/Subject.java | 14 ++++++++ 5 files changed, 74 insertions(+) diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index c186b1f78c..e3e508164f 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -203,6 +203,7 @@ public Throwable getThrowable() { } @Override @Experimental + @Deprecated @SuppressWarnings("unchecked") public T[] getValues(T[] a) { Object v = lastValue; diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index 1197048c3f..6ec0af1608 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -155,23 +155,44 @@ public Throwable getThrowable() { return null; } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public boolean hasValue() { return false; } + + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public T getValue() { return null; } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public Object[] getValues() { return new Object[0]; } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public T[] getValues(T[] a) { if (a.length > 0) { a[0] = null; diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index c3779dac2d..f2230f4bba 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -1162,7 +1162,9 @@ public boolean hasValue() { public T[] getValues(T[] a) { return state.toArray(a); } + @Override + @Experimental public T getValue() { return state.latest(); } diff --git a/src/main/java/rx/subjects/SerializedSubject.java b/src/main/java/rx/subjects/SerializedSubject.java index edf4caeefe..6dd5a46592 100644 --- a/src/main/java/rx/subjects/SerializedSubject.java +++ b/src/main/java/rx/subjects/SerializedSubject.java @@ -69,38 +69,74 @@ public void onNext(T t) { public boolean hasObservers() { return actual.hasObservers(); } + + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public boolean hasCompleted() { return actual.hasCompleted(); } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public boolean hasThrowable() { return actual.hasThrowable(); } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public boolean hasValue() { return actual.hasValue(); } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public Throwable getThrowable() { return actual.getThrowable(); } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public T getValue() { return actual.getValue(); } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public Object[] getValues() { return actual.getValues(); } + /** + * {@inheritDoc} + * @deprecated this method is scheduled to be removed in the next release + */ @Override @Experimental + @Deprecated public T[] getValues(T[] a) { return actual.getValues(a); } diff --git a/src/main/java/rx/subjects/Subject.java b/src/main/java/rx/subjects/Subject.java index 075dfe8e93..b220cc1b51 100644 --- a/src/main/java/rx/subjects/Subject.java +++ b/src/main/java/rx/subjects/Subject.java @@ -64,8 +64,10 @@ public final SerializedSubject toSerialized() { * * @return {@code true} if the subject has received a throwable through {@code onError}. * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + * @deprecated this method will be moved to each Subject class individually in the next release */ @Experimental + @Deprecated public boolean hasThrowable() { throw new UnsupportedOperationException(); } @@ -75,8 +77,10 @@ public boolean hasThrowable() { * * @return {@code true} if the subject completed normally via {@code onCompleted} * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + * @deprecated this method will be moved to each Subject class individually in the next release */ @Experimental + @Deprecated public boolean hasCompleted() { throw new UnsupportedOperationException(); } @@ -87,8 +91,10 @@ public boolean hasCompleted() { * @return the Throwable that terminated the Subject or {@code null} if the subject hasn't terminated yet or * if it terminated normally. * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + * @deprecated this method will be moved to each Subject class individually in the next release */ @Experimental + @Deprecated public Throwable getThrowable() { throw new UnsupportedOperationException(); } @@ -101,8 +107,10 @@ public Throwable getThrowable() { * * @return {@code true} if and only if the subject has some value but not an error * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + * @deprecated this method will be moved to each Subject class individually in the next release */ @Experimental + @Deprecated public boolean hasValue() { throw new UnsupportedOperationException(); } @@ -117,8 +125,10 @@ public boolean hasValue() { * @return the current value or {@code null} if the Subject doesn't have a value, has terminated with an * exception or has an actual {@code null} as a value. * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + * @deprecated this method will be moved to each Subject class individually in the next release */ @Experimental + @Deprecated public T getValue() { throw new UnsupportedOperationException(); } @@ -130,9 +140,11 @@ public T getValue() { * * @return a snapshot of the currently buffered non-terminal events. * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + * @deprecated this method will be moved to each Subject class individually in the next release */ @SuppressWarnings("unchecked") @Experimental + @Deprecated public Object[] getValues() { T[] r = getValues((T[])EMPTY_ARRAY); if (r == EMPTY_ARRAY) { @@ -152,8 +164,10 @@ public Object[] getValues() { * @param a the array to fill in * @return the array {@code a} if it had enough capacity or a new array containing the available values * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + * @deprecated this method will be moved to each Subject class individually in the next release */ @Experimental + @Deprecated public T[] getValues(T[] a) { throw new UnsupportedOperationException(); } From 2d832a4f5d48f74c18d7e5438f8369e7bd0e945d Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Thu, 8 Oct 2015 06:08:03 +0300 Subject: [PATCH 401/857] Add Single.doOnError() --- src/main/java/rx/Single.java | 37 +++++++++++++++++ src/test/java/rx/SingleTest.java | 69 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 7fbf369b79..4324d32acf 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -31,6 +31,7 @@ import rx.functions.Func8; import rx.functions.Func9; import rx.internal.operators.OnSubscribeToObservableFuture; +import rx.internal.operators.OperatorDoOnEach; import rx.internal.operators.OperatorMap; import rx.internal.operators.OperatorObserveOn; import rx.internal.operators.OperatorOnErrorReturn; @@ -1789,4 +1790,40 @@ public final Single zipWith(Single other, Func2 + * In case the onError action throws, the downstream will receive a composite exception containing + * the original exception and the exception thrown by onError. + *

+ * + *

+ *
Scheduler:
+ *
{@code doOnError} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param onError + * the action to invoke if the source {@link Single} calls {@code onError} + * @return the source {@link Single} with the side-effecting behavior applied + * @see ReactiveX operators documentation: Do + */ + @Experimental + public final Single doOnError(final Action1 onError) { + Observer observer = new Observer() { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + onError.call(e); + } + + @Override + public void onNext(T t) { + } + }; + + return lift(new OperatorDoOnEach(observer)); + } } diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 1efd1ae5a7..7d8fe2dc22 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -13,8 +13,13 @@ package rx; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import java.util.Arrays; import java.util.concurrent.CountDownLatch; @@ -26,7 +31,9 @@ import org.junit.Test; import rx.Single.OnSubscribe; +import rx.exceptions.CompositeException; import rx.functions.Action0; +import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; import rx.observers.TestSubscriber; @@ -461,4 +468,66 @@ public void testToObservable() { ts.assertValue("a"); ts.assertCompleted(); } + + @Test + public void doOnErrorShouldNotCallActionIfNoErrorHasOccurred() { + Action1 action = mock(Action1.class); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .just("value") + .doOnError(action) + .subscribe(testSubscriber); + + testSubscriber.assertValue("value"); + testSubscriber.assertNoErrors(); + + verifyZeroInteractions(action); + } + + @Test + public void doOnErrorShouldCallActionIfErrorHasOccurred() { + Action1 action = mock(Action1.class); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Throwable error = new IllegalStateException(); + + Single + .error(error) + .doOnError(action) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(error); + + verify(action).call(error); + } + + @Test + public void doOnErrorShouldThrowCompositeExceptionIfOnErrorActionThrows() { + Action1 action = mock(Action1.class); + + + Throwable error = new RuntimeException(); + Throwable exceptionFromOnErrorAction = new IllegalStateException(); + doThrow(exceptionFromOnErrorAction).when(action).call(error); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .error(error) + .doOnError(action) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + CompositeException compositeException = (CompositeException) testSubscriber.getOnErrorEvents().get(0); + + assertEquals(2, compositeException.getExceptions().size()); + assertSame(error, compositeException.getExceptions().get(0)); + assertSame(exceptionFromOnErrorAction, compositeException.getExceptions().get(1)); + + verify(action).call(error); + } } From 3c045c7867d03e60e5617832541822102e4a3ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 8 Oct 2015 18:17:53 +0200 Subject: [PATCH 402/857] BlockingObservable + subscribe methods. --- .../rx/observables/BlockingObservable.java | 256 +++++++++++++++++- .../observables/BlockingObservableTest.java | 164 +++++++++-- 2 files changed, 386 insertions(+), 34 deletions(-) diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 7eced68981..805e217bbe 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -15,23 +15,19 @@ */ package rx.observables; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; +import rx.*; import rx.Observable; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.internal.operators.BlockingOperatorLatest; -import rx.internal.operators.BlockingOperatorMostRecent; -import rx.internal.operators.BlockingOperatorNext; -import rx.internal.operators.BlockingOperatorToFuture; -import rx.internal.operators.BlockingOperatorToIterator; +import rx.Observer; +import rx.annotations.Experimental; +import rx.exceptions.OnErrorNotImplementedException; +import rx.functions.*; +import rx.internal.operators.*; import rx.internal.util.UtilityFunctions; +import rx.subscriptions.Subscriptions; /** * {@code BlockingObservable} is a variety of {@link Observable} that provides blocking operators. It can be @@ -83,12 +79,16 @@ public static BlockingObservable from(final Observable o) { * need the {@link Subscriber#onCompleted()} or {@link Subscriber#onError(Throwable)} methods. If the * underlying Observable terminates with an error, rather than calling {@code onError}, this method will * throw an exception. - * + * + *

The difference between this method and {@link #subscribe(Action1)} is that the {@code onNext} action + * is executed on the emission thread instead of the current thread. + * * @param onNext * the {@link Action1} to invoke for each item emitted by the {@code BlockingObservable} * @throws RuntimeException * if an error occurs * @see ReactiveX documentation: Subscribe + * @see #subscribe(Action1) */ public void forEach(final Action1 onNext) { final CountDownLatch latch = new CountDownLatch(1); @@ -477,4 +477,232 @@ private void awaitForComplete(CountDownLatch latch, Subscription subscription) { throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); } } + + /** + * Runs the source observable to a terminal event, ignoring any values and rethrowing any exception. + */ + @Experimental + public void run() { + final CountDownLatch cdl = new CountDownLatch(1); + final Throwable[] error = { null }; + Subscription s = o.subscribe(new Subscriber() { + @Override + public void onNext(T t) { + + } + @Override + public void onError(Throwable e) { + error[0] = e; + cdl.countDown(); + } + + @Override + public void onCompleted() { + cdl.countDown(); + } + }); + + awaitForComplete(cdl, s); + Throwable e = error[0]; + if (e != null) { + if (e instanceof RuntimeException) { + throw (RuntimeException)e; + } else { + throw new RuntimeException(e); + } + } + } + + /** + * Subscribes to the source and calls back the Observer methods on the current thread. + * @param observer the observer to call event methods on + */ + @Experimental + public void subscribe(Observer observer) { + final NotificationLite nl = NotificationLite.instance(); + final BlockingQueue queue = new LinkedBlockingQueue(); + + Subscription s = o.subscribe(new Subscriber() { + @Override + public void onNext(T t) { + queue.offer(nl.next(t)); + } + @Override + public void onError(Throwable e) { + queue.offer(nl.error(e)); + } + @Override + public void onCompleted() { + queue.offer(nl.completed()); + } + }); + + try { + for (;;) { + Object o = queue.poll(); + if (o == null) { + o = queue.take(); + } + if (nl.accept(observer, o)) { + return; + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + observer.onError(e); + } finally { + s.unsubscribe(); + } + } + + /** Constant to indicate the onStart method should be called. */ + private static final Object ON_START = new Object(); + + /** Constant indicating the setProducer method should be called. */ + private static final Object SET_PRODUCER = new Object(); + + /** Indicates an unsubscripton happened */ + private static final Object UNSUBSCRIBE = new Object(); + + /** + * Subscribes to the source and calls the Subscriber methods on the current thread. + *

+ * The unsubscription and backpressure is composed through. + * @param subscriber the subscriber to forward events and calls to in the current thread + */ + @Experimental + public void subscribe(Subscriber subscriber) { + final NotificationLite nl = NotificationLite.instance(); + final BlockingQueue queue = new LinkedBlockingQueue(); + final Producer[] theProducer = { null }; + + Subscriber s = new Subscriber() { + @Override + public void onNext(T t) { + queue.offer(nl.next(t)); + } + @Override + public void onError(Throwable e) { + queue.offer(nl.error(e)); + } + @Override + public void onCompleted() { + queue.offer(nl.completed()); + } + + @Override + public void setProducer(Producer p) { + theProducer[0] = p; + queue.offer(SET_PRODUCER); + } + + @Override + public void onStart() { + queue.offer(ON_START); + } + }; + + subscriber.add(s); + subscriber.add(Subscriptions.create(new Action0() { + @Override + public void call() { + queue.offer(UNSUBSCRIBE); + } + })); + + o.subscribe(s); + + try { + for (;;) { + if (subscriber.isUnsubscribed()) { + break; + } + Object o = queue.poll(); + if (o == null) { + o = queue.take(); + } + if (subscriber.isUnsubscribed() || o == UNSUBSCRIBE) { + break; + } + if (o == ON_START) { + subscriber.onStart(); + } else + if (o == SET_PRODUCER) { + subscriber.setProducer(theProducer[0]); + } else + if (nl.accept(subscriber, o)) { + return; + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + subscriber.onError(e); + } finally { + s.unsubscribe(); + } + } + + /** + * Runs the source observable to a terminal event, ignoring any values and rethrowing any exception. + */ + @Experimental + public void subscribe() { + run(); + } + + /** + * Subscribes to the source and calls the given action on the current thread and rethrows any exception wrapped + * into OnErrorNotImplementedException. + * + *

The difference between this method and {@link #forEach(Action1)} is that the + * action is always executed on the current thread. + * + * @param onNext the callback action for each source value + * @see #forEach(Action1) + */ + @Experimental + public void subscribe(final Action1 onNext) { + subscribe(onNext, new Action1() { + @Override + public void call(Throwable t) { + throw new OnErrorNotImplementedException(t); + } + }, Actions.empty()); + } + + /** + * Subscribes to the source and calls the given actions on the current thread. + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + */ + @Experimental + public void subscribe(final Action1 onNext, final Action1 onError) { + subscribe(onNext, onError, Actions.empty()); + } + + /** + * Subscribes to the source and calls the given actions on the current thread. + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param onCompleted the callback action for the completion event. + */ + @Experimental + public void subscribe(final Action1 onNext, final Action1 onError, final Action0 onCompleted) { + subscribe(new Observer() { + @Override + public void onNext(T t) { + onNext.call(t); + } + + @Override + public void onError(Throwable e) { + onError.call(e); + } + + @Override + public void onCompleted() { + onCompleted.call(); + } + }); + } } diff --git a/src/test/java/rx/observables/BlockingObservableTest.java b/src/test/java/rx/observables/BlockingObservableTest.java index 4328461d80..72963f76ae 100644 --- a/src/test/java/rx/observables/BlockingObservableTest.java +++ b/src/test/java/rx/observables/BlockingObservableTest.java @@ -15,34 +15,25 @@ */ package rx.observables; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; +import org.junit.*; +import org.mockito.*; + import rx.Observable; import rx.Observable.OnSubscribe; 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.Schedulers; import rx.subscriptions.Subscriptions; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - public class BlockingObservableTest { @Mock @@ -641,4 +632,137 @@ private InterruptedException getInterruptedExceptionOrNull() { } + @Test + public void testRun() { + Observable.just(1).observeOn(Schedulers.computation()).toBlocking().run(); + } + + @Test(expected = TestException.class) + public void testRunException() { + Observable.error(new TestException()).observeOn(Schedulers.computation()).toBlocking().run(); + } + + @Test + public void testRunIOException() { + try { + Observable.error(new IOException()).observeOn(Schedulers.computation()).toBlocking().run(); + fail("No exception thrown"); + } catch (RuntimeException ex) { + if (ex.getCause() instanceof IOException) { + return; + } + fail("Bad exception type: " + ex + ", " + ex.getCause()); + } + } + + @Test + public void testSubscriberBackpressure() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + request(2); + } + + @Override + public void onNext(Integer t) { + super.onNext(t); + unsubscribe(); + } + }; + + Observable.range(1, 10).observeOn(Schedulers.computation()).toBlocking().subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotCompleted(); + ts.assertValue(1); + } + + @Test(expected = OnErrorNotImplementedException.class) + public void testOnErrorNotImplemented() { + Observable.error(new TestException()).observeOn(Schedulers.computation()).toBlocking().subscribe(Actions.empty()); + } + + @Test + public void testSubscribeCallback1() { + final boolean[] valueReceived = { false }; + Observable.just(1).observeOn(Schedulers.computation()).toBlocking().subscribe(new Action1() { + @Override + public void call(Integer t) { + valueReceived[0] = true; + assertEquals((Integer)1, t); + } + }); + + assertTrue(valueReceived[0]); + } + + @Test + public void testSubscribeCallback2() { + final boolean[] received = { false }; + Observable.error(new TestException()).observeOn(Schedulers.computation()).toBlocking() + .subscribe(new Action1() { + @Override + public void call(Object t) { + fail("Value emitted: " + t); + } + }, new Action1() { + @Override + public void call(Throwable t) { + received[0] = true; + assertEquals(TestException.class, t.getClass()); + } + }); + + assertTrue(received[0]); + } + + @Test + public void testSubscribeCallback3() { + final boolean[] received = { false, false }; + Observable.just(1).observeOn(Schedulers.computation()).toBlocking().subscribe(new Action1() { + @Override + public void call(Integer t) { + received[0] = true; + assertEquals((Integer)1, t); + } + }, new Action1() { + @Override + public void call(Throwable t) { + t.printStackTrace(); + fail("Exception received!"); + } + }, new Action0() { + @Override + public void call() { + received[1] = true; + } + }); + + assertTrue(received[0]); + assertTrue(received[1]); + } + @Test + public void testSubscribeCallback3Error() { + final TestSubscriber ts = TestSubscriber.create(); + Observable.error(new TestException()).observeOn(Schedulers.computation()).toBlocking().subscribe(new Action1() { + @Override + public void call(Object t) { + ts.onNext(t); + } + }, new Action1() { + @Override + public void call(Throwable t) { + ts.onError(t); + } + }, new Action0() { + @Override + public void call() { + ts.onCompleted(); + } + }); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } } From f9cf9dd7a3f5d235ff8d67d5f26b9bf68232ddda Mon Sep 17 00:00:00 2001 From: George Campbell Date: Thu, 8 Oct 2015 10:21:52 -0700 Subject: [PATCH 403/857] Update README.md Slight change to make the distinction between `@Beta` and `@Experimental` explicit and meaningful. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5dcb2199a8..aca2675593 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ Patch 1.x.y increments (such as 1.0.0 -> 1.0.1, 1.3.1 -> 1.3.2, etc) will occur #### @Beta -APIs marked with the `@Beta` annotation at the class or method level are subject to change. They can be modified in any way, or even removed, at any time. If your code is a library itself (i.e. it is used on the CLASSPATH of users outside your own control), you should not use beta APIs, unless you repackage them (e.g. using ProGuard, shading, etc). +APIs marked with the `@Beta` annotation at the class or method level are subject to change. They can be modified in any way, or even removed in any major or minor release but not in a patch release. If your code is a library itself (i.e. it is used on the CLASSPATH of users outside your own control), you should not use beta APIs, unless you repackage them (e.g. using ProGuard, shading, etc). #### @Experimental -APIs marked with the `@Experimental` annotation at the class or method level will almost certainly change. They can be modified in any way, or even removed, at any time. You should not use or rely on them in any production code. They are purely to allow broad testing and feedback. +APIs marked with the `@Experimental` annotation at the class or method level will almost certainly change. They can be modified in any way, or even removed in any major, minor or, patch release. You should not use or rely on them in any production code. They are purely to allow broad testing and feedback. #### @Deprecated -APIs marked with the `@Deprecated` annotation at the class or method level will remain supported until the next major release but it is recommended to stop using them. +APIs marked with the `@Deprecated` annotation at the class or method level will remain supported until the next major release but it is recommended to stop using them. APIs marked with `@Beta` and `@Experimental` will be marked as deprecated for at least one minor release before they removed in a minor or patch release respectively. #### rx.internal.* From 30df85bf3c7d90fbbb721b49e68b4e899f9dd6ce Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Thu, 8 Oct 2015 12:05:18 -0700 Subject: [PATCH 404/857] Renaming Observable#x to Observable#extend --- src/main/java/rx/Observable.java | 2 +- src/test/java/rx/ObservableConversionTest.java | 4 ++-- src/test/java/rx/ObservableTests.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index ebe421a120..02142ec6ce 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -119,7 +119,7 @@ public interface Operator extends Func1, Subscriber< * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental - public R x(Func1, ? extends R> conversion) { + public R extend(Func1, ? extends R> conversion) { return conversion.call(new OnSubscribe() { @Override public void call(Subscriber subscriber) { diff --git a/src/test/java/rx/ObservableConversionTest.java b/src/test/java/rx/ObservableConversionTest.java index 543c44780b..31880ea599 100644 --- a/src/test/java/rx/ObservableConversionTest.java +++ b/src/test/java/rx/ObservableConversionTest.java @@ -155,7 +155,7 @@ public void onNext(String t) { }}); List crewOfBattlestarGalactica = Arrays.asList(new Object[] {"William Adama", "Laura Roslin", "Lee Adama", new Cylon()}); Observable.from(crewOfBattlestarGalactica) - .x(new ConvertToCylonDetector()) + .extend(new ConvertToCylonDetector()) .beep(new Func1(){ @Override public Boolean call(Object t) { @@ -199,7 +199,7 @@ public Integer call(Integer k) { return i + k; }}); }}) - .x(new Func1, ConcurrentLinkedQueue>() { + .extend(new Func1, ConcurrentLinkedQueue>() { @Override public ConcurrentLinkedQueue call(OnSubscribe onSubscribe) { final ConcurrentLinkedQueue q = new ConcurrentLinkedQueue(); diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index 55e43896d3..d59e8c41a9 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -1161,7 +1161,7 @@ public void testForEachWithNull() { public void testExtend() { final TestSubscriber subscriber = new TestSubscriber(); final Object value = new Object(); - Observable.just(value).x(new Func1,Object>(){ + Observable.just(value).extend(new Func1,Object>(){ @Override public Object call(OnSubscribe onSubscribe) { onSubscribe.call(subscriber); From f64233fb2d9bcb77e9249d3bc497fd9d110e6a9f Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Thu, 8 Oct 2015 05:58:36 +0300 Subject: [PATCH 405/857] Add Single.fromCallable() --- src/main/java/rx/Single.java | 38 ++++++++++++++++++++++++++++++ src/test/java/rx/SingleTest.java | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 4324d32acf..3701d93189 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -12,6 +12,7 @@ */ package rx; +import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -605,6 +606,43 @@ public final static Single from(Future future, Scheduler sch return new Single(OnSubscribeToObservableFuture.toObservableFuture(future)).subscribeOn(scheduler); } + /** + * Returns a {@link Single} that invokes passed function and emits its result for each new Observer that subscribes. + *

+ * Allows you to defer execution of passed function until Observer subscribes to the {@link Single}. + * It makes passed function "lazy". + * Result of the function invocation will be emitted by the {@link Single}. + *

+ *
Scheduler:
+ *
{@code fromCallable} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param func + * function which execution should be deferred, it will be invoked when Observer will subscribe to the {@link Single}. + * @param + * 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 + public static Single fromCallable(final Callable func) { + return create(new OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + final T value; + + try { + value = func.call(); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + singleSubscriber.onError(t); + return; + } + + singleSubscriber.onSuccess(value); + } + }); + } + /** * Returns a {@code Single} that emits a specified item. *

diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 7d8fe2dc22..f78151b094 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -20,8 +20,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -530,4 +532,42 @@ public void doOnErrorShouldThrowCompositeExceptionIfOnErrorActionThrows() { verify(action).call(error); } + + @Test + public void shouldEmitValueFromCallable() throws Exception { + Callable callable = mock(Callable.class); + + when(callable.call()).thenReturn("value"); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .fromCallable(callable) + .subscribe(testSubscriber); + + testSubscriber.assertValue("value"); + testSubscriber.assertNoErrors(); + + verify(callable).call(); + } + + @Test + public void shouldPassErrorFromCallable() throws Exception { + Callable callable = mock(Callable.class); + + Throwable error = new IllegalStateException(); + + when(callable.call()).thenThrow(error); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .fromCallable(callable) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(error); + + verify(callable).call(); + } } From 0c0a18f380bd006f8c6ea385cf1385da73f16d0a Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Thu, 8 Oct 2015 05:40:12 +0300 Subject: [PATCH 406/857] Add Single.doOnSuccess() --- src/main/java/rx/Single.java | 34 ++++++++++++++ src/test/java/rx/SingleTest.java | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 3701d93189..6817a4e283 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1864,4 +1864,38 @@ public void onNext(T t) { return lift(new OperatorDoOnEach(observer)); } + + /** + * Modifies the source {@link Single} so that it invokes an action when it calls {@code onSuccess}. + *

+ * + *

+ *
Scheduler:
+ *
{@code doOnSuccess} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param onSuccess + * the action to invoke when the source {@link Single} calls {@code onSuccess} + * @return the source {@link Single} with the side-effecting behavior applied + * @see ReactiveX operators documentation: Do + */ + @Experimental + public final Single doOnSuccess(final Action1 onSuccess) { + Observer observer = new Observer() { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(T t) { + onSuccess.call(t); + } + }; + + return lift(new OperatorDoOnEach(observer)); + } } diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index f78151b094..de1a38f0ca 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -16,6 +16,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -570,4 +571,81 @@ public void shouldPassErrorFromCallable() throws Exception { verify(callable).call(); } + + @Test + public void doOnSuccessShouldInvokeAction() { + Action1 action = mock(Action1.class); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .just("value") + .doOnSuccess(action) + .subscribe(testSubscriber); + + testSubscriber.assertValue("value"); + testSubscriber.assertNoErrors(); + + verify(action).call(eq("value")); + } + + @Test + public void doOnSuccessShouldPassErrorFromActionToSubscriber() { + Action1 action = mock(Action1.class); + + Throwable error = new IllegalStateException(); + doThrow(error).when(action).call(eq("value")); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .just("value") + .doOnSuccess(action) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(error); + + verify(action).call(eq("value")); + } + + @Test + public void doOnSuccessShouldNotCallActionIfSingleThrowsError() { + Action1 action = mock(Action1.class); + + Throwable error = new IllegalStateException(); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .error(error) + .doOnSuccess(action) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(error); + + verifyZeroInteractions(action); + } + + @Test + public void doOnSuccessShouldNotSwallowExceptionThrownByAction() { + Action1 action = mock(Action1.class); + + Throwable exceptionFromAction = new IllegalStateException(); + + doThrow(exceptionFromAction).when(action).call(eq("value")); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .just("value") + .doOnSuccess(action) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(exceptionFromAction); + + verify(action).call(eq("value")); + } } From 1385cd8d4a1ff96a43b32ffd2f46209515473a8c Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Fri, 9 Oct 2015 11:55:47 -0700 Subject: [PATCH 407/857] Removed the alias BlockingObservable#run --- src/main/java/rx/observables/BlockingObservable.java | 10 +--------- .../java/rx/observables/BlockingObservableTest.java | 6 +++--- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 805e217bbe..5463e9696e 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -482,7 +482,7 @@ private void awaitForComplete(CountDownLatch latch, Subscription subscription) { * Runs the source observable to a terminal event, ignoring any values and rethrowing any exception. */ @Experimental - public void run() { + public void subscribe() { final CountDownLatch cdl = new CountDownLatch(1); final Throwable[] error = { null }; Subscription s = o.subscribe(new Subscriber() { @@ -642,14 +642,6 @@ public void call() { } } - /** - * Runs the source observable to a terminal event, ignoring any values and rethrowing any exception. - */ - @Experimental - public void subscribe() { - run(); - } - /** * Subscribes to the source and calls the given action on the current thread and rethrows any exception wrapped * into OnErrorNotImplementedException. diff --git a/src/test/java/rx/observables/BlockingObservableTest.java b/src/test/java/rx/observables/BlockingObservableTest.java index 72963f76ae..c20eabd01d 100644 --- a/src/test/java/rx/observables/BlockingObservableTest.java +++ b/src/test/java/rx/observables/BlockingObservableTest.java @@ -634,18 +634,18 @@ private InterruptedException getInterruptedExceptionOrNull() { @Test public void testRun() { - Observable.just(1).observeOn(Schedulers.computation()).toBlocking().run(); + Observable.just(1).observeOn(Schedulers.computation()).toBlocking().subscribe(); } @Test(expected = TestException.class) public void testRunException() { - Observable.error(new TestException()).observeOn(Schedulers.computation()).toBlocking().run(); + Observable.error(new TestException()).observeOn(Schedulers.computation()).toBlocking().subscribe(); } @Test public void testRunIOException() { try { - Observable.error(new IOException()).observeOn(Schedulers.computation()).toBlocking().run(); + Observable.error(new IOException()).observeOn(Schedulers.computation()).toBlocking().subscribe(); fail("No exception thrown"); } catch (RuntimeException ex) { if (ex.getCause() instanceof IOException) { From a7ed27eafa8e7a3fc36f1ff46e403045facb21c3 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sat, 10 Oct 2015 20:30:46 +0300 Subject: [PATCH 408/857] Add action != null check in OperatorFinally --- .../java/rx/internal/operators/OperatorFinally.java | 3 +++ .../rx/internal/operators/OperatorFinallyTest.java | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/rx/internal/operators/OperatorFinally.java b/src/main/java/rx/internal/operators/OperatorFinally.java index 64ee03d4a4..5f870f8f37 100644 --- a/src/main/java/rx/internal/operators/OperatorFinally.java +++ b/src/main/java/rx/internal/operators/OperatorFinally.java @@ -33,6 +33,9 @@ public final class OperatorFinally implements Operator { final Action0 action; public OperatorFinally(Action0 action) { + if (action == null) { + throw new NullPointerException("Action can not be null"); + } this.action = action; } diff --git a/src/test/java/rx/internal/operators/OperatorFinallyTest.java b/src/test/java/rx/internal/operators/OperatorFinallyTest.java index 5403e7ebe6..e89ee74468 100644 --- a/src/test/java/rx/internal/operators/OperatorFinallyTest.java +++ b/src/test/java/rx/internal/operators/OperatorFinallyTest.java @@ -15,6 +15,8 @@ */ 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; @@ -53,4 +55,14 @@ public void testFinallyCalledOnComplete() { public void testFinallyCalledOnError() { checkActionCalled(Observable. error(new RuntimeException("expected"))); } + + @Test + public void nullActionShouldBeCheckedInConstructor() { + try { + new OperatorFinally(null); + fail(); + } catch (NullPointerException expected) { + assertEquals("Action can not be null", expected.getMessage()); + } + } } From 130fcbef8e08833db1158577a78a32bdf8a78bb6 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 12 Oct 2015 08:13:29 +0200 Subject: [PATCH 409/857] Better null tolerance in rx.exceptions.*Exception classes. --- .../MissingBackpressureException.java | 8 ++ .../OnCompletedFailedException.java | 24 ++++- .../rx/exceptions/OnErrorFailedException.java | 8 +- .../OnErrorNotImplementedException.java | 8 +- .../java/rx/exceptions/OnErrorThrowable.java | 11 ++- .../UnsubscribeFailedException.java | 24 ++++- .../rx/exceptions/ExceptionsNullTest.java | 93 +++++++++++++++++++ 7 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 src/test/java/rx/exceptions/ExceptionsNullTest.java diff --git a/src/main/java/rx/exceptions/MissingBackpressureException.java b/src/main/java/rx/exceptions/MissingBackpressureException.java index 86940a5919..b113d6536c 100644 --- a/src/main/java/rx/exceptions/MissingBackpressureException.java +++ b/src/main/java/rx/exceptions/MissingBackpressureException.java @@ -48,9 +48,17 @@ public class MissingBackpressureException extends Exception { private static final long serialVersionUID = 7250870679677032194L; + /** + * Constructs the exception without any custom message. + */ public MissingBackpressureException() { + } + /** + * Constructs the exception with the given customized message. + * @param message the customized message + */ public MissingBackpressureException(String message) { super(message); } diff --git a/src/main/java/rx/exceptions/OnCompletedFailedException.java b/src/main/java/rx/exceptions/OnCompletedFailedException.java index 37632d86c6..2586c9b692 100644 --- a/src/main/java/rx/exceptions/OnCompletedFailedException.java +++ b/src/main/java/rx/exceptions/OnCompletedFailedException.java @@ -15,15 +15,35 @@ */ package rx.exceptions; +import rx.Subscriber; + +/** + * Represents an exception used to re-throw errors thrown from {@link Subscriber#onCompleted()}. + */ public final class OnCompletedFailedException extends RuntimeException { private static final long serialVersionUID = 8622579378868820554L; + /** + * Wraps the {@code Throwable} before it is to be re-thrown as an {@code OnCompletedFailedException}. + * + * @param e + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed + */ public OnCompletedFailedException(Throwable throwable) { - super(throwable); + super(throwable != null ? throwable : new NullPointerException()); } + /** + * Customizes the {@code Throwable} with a custom message and wraps it before it is to be re-thrown as an + * {@code OnCompletedFailedException}. + * + * @param message + * the message to assign to the {@code Throwable} to re-throw + * @param e + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed + */ public OnCompletedFailedException(String message, Throwable throwable) { - super(message, throwable); + super(message, throwable != null ? throwable : new NullPointerException()); } } diff --git a/src/main/java/rx/exceptions/OnErrorFailedException.java b/src/main/java/rx/exceptions/OnErrorFailedException.java index 7ba45719d4..a79000c21d 100644 --- a/src/main/java/rx/exceptions/OnErrorFailedException.java +++ b/src/main/java/rx/exceptions/OnErrorFailedException.java @@ -32,19 +32,19 @@ public class OnErrorFailedException extends RuntimeException { * @param message * the message to assign to the {@code Throwable} to re-throw * @param e - * the {@code Throwable} to re-throw + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed */ public OnErrorFailedException(String message, Throwable e) { - super(message, e); + super(message, e != null ? e : new NullPointerException()); } /** * Wraps the {@code Throwable} before it is to be re-thrown as an {@code OnErrorFailedException}. * * @param e - * the {@code Throwable} to re-throw + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed */ public OnErrorFailedException(Throwable e) { - super(e.getMessage(), e); + super(e != null ? e.getMessage() : null, e != null ? e : new NullPointerException()); } } diff --git a/src/main/java/rx/exceptions/OnErrorNotImplementedException.java b/src/main/java/rx/exceptions/OnErrorNotImplementedException.java index 4e997938f7..d707a791fa 100644 --- a/src/main/java/rx/exceptions/OnErrorNotImplementedException.java +++ b/src/main/java/rx/exceptions/OnErrorNotImplementedException.java @@ -40,19 +40,19 @@ public class OnErrorNotImplementedException extends RuntimeException { * @param message * the message to assign to the {@code Throwable} to re-throw * @param e - * the {@code Throwable} to re-throw + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed */ public OnErrorNotImplementedException(String message, Throwable e) { - super(message, e); + super(message, e != null ? e : new NullPointerException()); } /** * Wraps the {@code Throwable} before it is to be re-thrown as an {@code OnErrorNotImplementedException}. * * @param e - * the {@code Throwable} to re-throw + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed */ public OnErrorNotImplementedException(Throwable e) { - super(e.getMessage(), e); + super(e != null ? e.getMessage() : null, e != null ? e : new NullPointerException()); } } diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java index e54a9a80ce..52ce45ed2a 100644 --- a/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -69,16 +69,18 @@ public boolean isValueNull() { * Converts a {@link Throwable} into an {@link OnErrorThrowable}. * * @param t - * the {@code Throwable} to convert + * the {@code Throwable} to convert; if null, a NullPointerException is constructed * @return an {@code OnErrorThrowable} representation of {@code t} */ public static OnErrorThrowable from(Throwable t) { + if (t == null) { + t = new NullPointerException(); + } Throwable cause = Exceptions.getFinalCause(t); if (cause instanceof OnErrorThrowable.OnNextValue) { return new OnErrorThrowable(t, ((OnNextValue) cause).getValue()); - } else { - return new OnErrorThrowable(t); } + return new OnErrorThrowable(t); } /** @@ -93,6 +95,9 @@ public static OnErrorThrowable from(Throwable t) { * cause */ public static Throwable addValueAsLastCause(Throwable e, Object value) { + if (e == null) { + e = new NullPointerException(); + } Throwable lastCause = Exceptions.getFinalCause(e); if (lastCause != null && lastCause instanceof OnNextValue) { // purposefully using == for object reference check diff --git a/src/main/java/rx/exceptions/UnsubscribeFailedException.java b/src/main/java/rx/exceptions/UnsubscribeFailedException.java index 8b01df8aa3..69eb260ea2 100644 --- a/src/main/java/rx/exceptions/UnsubscribeFailedException.java +++ b/src/main/java/rx/exceptions/UnsubscribeFailedException.java @@ -15,16 +15,36 @@ */ package rx.exceptions; +import rx.Subscriber; + +/** + * Represents an exception used to re-throw errors thrown from {@link Subscriber#unsubscribe()}. + */ public final class UnsubscribeFailedException extends RuntimeException { private static final long serialVersionUID = 4594672310593167598L; + /** + * Wraps the {@code Throwable} before it is to be re-thrown as an {@code OnErrorFailedException}. + * + * @param throwable + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed + */ public UnsubscribeFailedException(Throwable throwable) { - super(throwable); + super(throwable != null ? throwable : new NullPointerException()); } + /** + * Customizes the {@code Throwable} with a custom message and wraps it before it is to be re-thrown as an + * {@code UnsubscribeFailedException}. + * + * @param message + * the message to assign to the {@code Throwable} to re-throw + * @param throwable + * the {@code Throwable} to re-throw; if null, a NullPointerException is constructed + */ public UnsubscribeFailedException(String message, Throwable throwable) { - super(message, throwable); + super(message, throwable != null ? throwable : new NullPointerException()); } } diff --git a/src/test/java/rx/exceptions/ExceptionsNullTest.java b/src/test/java/rx/exceptions/ExceptionsNullTest.java new file mode 100644 index 0000000000..e704d7cf7c --- /dev/null +++ b/src/test/java/rx/exceptions/ExceptionsNullTest.java @@ -0,0 +1,93 @@ +/** + * 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.exceptions; + +import org.junit.*; + +/** + * Checks the Exception classes to verify they don't crash with null argument + */ +public class ExceptionsNullTest { + + @Test + public void testOnCompleteFailedExceptionNull() { + Throwable t = new OnCompletedFailedException(null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testOnCompleteFailedExceptionMessageAndNull() { + Throwable t = new OnCompletedFailedException("Message", null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testOnErrorFailedExceptionNull() { + Throwable t = new OnErrorFailedException(null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testOnErrorFailedExceptionMessageAndNull() { + Throwable t = new OnErrorFailedException("Message", null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testUnsubscribeFailedExceptionNull() { + Throwable t = new UnsubscribeFailedException(null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testUnsubscribeFailedExceptionMessageAndNull() { + Throwable t = new UnsubscribeFailedException("Message", null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testOnErrorNotImplementedExceptionNull() { + Throwable t = new OnErrorNotImplementedException(null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testOnErrorNotImplementedExceptionMessageAndNull() { + Throwable t = new OnErrorNotImplementedException("Message", null); + + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testOnErrorThrowableFrom() { + Throwable t = OnErrorThrowable.from(null); + Assert.assertTrue(t.getCause() instanceof NullPointerException); + } + + @Test + public void testOnErrorThrowableAddValueAsLastCause() { + Throwable t = OnErrorThrowable.addValueAsLastCause(null, "value"); + Assert.assertTrue(t instanceof NullPointerException); + } + +} From 5e7b3013e13007e8ce3bb3edf22b54534baa7f9c Mon Sep 17 00:00:00 2001 From: "David M. Gross" Date: Mon, 12 Oct 2015 10:07:53 -0700 Subject: [PATCH 410/857] correct URL of marble diagram image Fixes #3437 --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 02142ec6ce..8374b75fd5 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -217,7 +217,7 @@ public interface Transformer extends Func1, Observable> { * emits only a single item. If the source Observable emits more than one item or no items, notify of an * {@code IllegalArgumentException} or {@code NoSuchElementException} respectively. *

- * + * *

*
Scheduler:
*
{@code toSingle} does not operate by default on a particular {@link Scheduler}.
From aef6b96e3c7a78480d46b7052499b2a101bd7fd6 Mon Sep 17 00:00:00 2001 From: "David M. Gross" Date: Mon, 12 Oct 2015 10:30:34 -0700 Subject: [PATCH 411/857] javadoc improvements largely to the new eager-concat methods --- src/main/java/rx/Observable.java | 231 +++++++++++--------- src/main/java/rx/exceptions/Exceptions.java | 4 +- 2 files changed, 128 insertions(+), 107 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 8374b75fd5..a0d93ccacf 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4362,9 +4362,10 @@ public final void onNext(T v) { /** * Modifies the source Observable so that it notifies an Observer for each item it emits. *

- * In case the onError of the supplied observer throws, the downstream will receive a composite exception containing - * the original exception and the exception thrown by onError. If the onNext or the onCompleted methods - * of the supplied observer throws, the downstream will be terminated and wil receive this thrown exception. + * In case the {@code onError} of the supplied observer throws, the downstream will receive a composite + * exception containing the original exception and the exception thrown by {@code onError}. If either the + * {@code onNext} or the {@code onCompleted} method of the supplied observer throws, the downstream will be + * terminated and will receive this thrown exception. *

* *

@@ -4384,8 +4385,8 @@ public final Observable doOnEach(Observer observer) { /** * Modifies the source Observable so that it invokes an action if it calls {@code onError}. *

- * In case the onError action throws, the downstream will receive a composite exception containing - * the original exception and the exception thrown by onError. + * In case the {@code onError} action throws, the downstream will receive a composite exception containing + * the original exception and the exception thrown by {@code onError}. *

* *

@@ -4560,16 +4561,15 @@ public final Observable doOnUnsubscribe(final Action0 unsubscribe) { } /** - * Concatenates up to 2 sources eagerly into a single stream of values. - * + * Concatenates two source Observables eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4577,6 +4577,8 @@ public final Observable doOnUnsubscribe(final Action0 unsubscribe) { * @param o1 the first source * @param o2 the second source * @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 @SuppressWarnings("unchecked") @@ -4585,16 +4587,15 @@ public static Observable concatEager(Observable o1, Observab } /** - * Concatenates up to 3 sources eagerly into a single stream of values. - * + * Concatenates three sources eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4603,6 +4604,8 @@ public static Observable concatEager(Observable o1, Observab * @param o2 the second source * @param o3 the third source * @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 @SuppressWarnings("unchecked") @@ -4614,16 +4617,15 @@ public static Observable concatEager( } /** - * Concatenates up to 4 sources eagerly into a single stream of values. - * + * Concatenates four sources eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4633,6 +4635,8 @@ public static Observable concatEager( * @param o3 the third source * @param o4 the fourth source * @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 @SuppressWarnings("unchecked") @@ -4644,16 +4648,15 @@ public static Observable concatEager( } /** - * Concatenates up to 5 sources eagerly into a single stream of values. - * + * Concatenates five sources eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4664,6 +4667,8 @@ public static Observable concatEager( * @param o4 the fourth source * @param o5 the fifth source * @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 @SuppressWarnings("unchecked") @@ -4676,16 +4681,15 @@ public static Observable concatEager( } /** - * Concatenates up to 6 sources eagerly into a single stream of values. - * + * Concatenates six sources eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4697,6 +4701,8 @@ public static Observable concatEager( * @param o5 the fifth source * @param o6 the sixth source * @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 @SuppressWarnings("unchecked") @@ -4709,16 +4715,15 @@ public static Observable concatEager( } /** - * Concatenates up to 7 sources eagerly into a single stream of values. - * + * Concatenates seven sources eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4731,6 +4736,8 @@ public static Observable concatEager( * @param o6 the sixth source * @param o7 the seventh source * @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 @SuppressWarnings("unchecked") @@ -4744,16 +4751,15 @@ public static Observable concatEager( } /** - * Concatenates up to 8 sources eagerly into a single stream of values. - * + * Concatenates eight sources eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4765,8 +4771,10 @@ public static Observable concatEager( * @param o5 the fifth source * @param o6 the sixth source * @param o7 the seventh source - * @param o8 the eight source + * @param o8 the eighth source * @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 @SuppressWarnings("unchecked") @@ -4780,16 +4788,15 @@ public static Observable concatEager( } /** - * Concatenates up to 9 sources eagerly into a single stream of values. - * + * Concatenates nine sources eagerly into a single stream of values. *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4801,9 +4808,11 @@ public static Observable concatEager( * @param o5 the fifth source * @param o6 the sixth source * @param o7 the seventh source - * @param o8 the eight source - * @param o9 the nine source + * @param o8 the eighth source + * @param o9 the ninth source * @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 @SuppressWarnings("unchecked") @@ -4819,21 +4828,22 @@ public static Observable concatEager( /** * Concatenates a sequence of Observables eagerly into a single stream of values. - * *

- * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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 sources a sequence of Observables that need to be eagerly concatenated * @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 @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -4843,15 +4853,14 @@ public static Observable concatEager(Iterable - * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4859,6 +4868,8 @@ public static Observable concatEager(Iterable Observable concatEager(Iterable - * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source Observables as they are observed. 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 values queued up in an unbounded buffer.
+ *
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 sources a sequence of Observables that need to be eagerly concatenated * @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 @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -4892,15 +4904,14 @@ public static Observable concatEager(Observable - * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source Observables as they are observed. 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 values queued up in an unbounded buffer.
+ *
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}.
*
@@ -4908,6 +4919,8 @@ public static Observable concatEager(Observable Observable concatEager(Observable - * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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 bec eagerly concatenated + * @param the value type + * @param mapper the function that maps a sequence of values into a sequence of Observables that will be + * eagerly concatenated * @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) { @@ -4939,23 +4955,26 @@ public final Observable concatMapEager(Func1 - * Eager concatenation means that once a Subscriber subscribes, all sources are subscribed to, their values fully buffered and - * they are drained in order, one after the previous completes. - * + * 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 values queued up in an unbounded buffer.
+ *
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 bec eagerly concatenated + * @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 * @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) { diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index 1b29838637..a0439028eb 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -157,6 +157,7 @@ public static Throwable getFinalCause(Throwable e) { * @param exceptions the collection of exceptions. If null or empty, no exception is thrown. * If the collection contains a single exception, that exception is either thrown as-is or wrapped into a * CompositeException. Multiple exceptions are wrapped into a CompositeException. + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public static void throwIfAny(List exceptions) { @@ -184,6 +185,7 @@ public static void throwIfAny(List exceptions) { * @param t the exception * @param o the observer to report to * @param value the value that caused the exception + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public static void throwOrReport(Throwable t, Observer o, Object value) { @@ -194,7 +196,7 @@ public static void throwOrReport(Throwable t, Observer o, Object value) { * Forwards a fatal exception or reports it to the given Observer. * @param t the exception * @param o the observer to report to - * @param value the value that caused the exception + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public static void throwOrReport(Throwable t, Observer o) { From f9d3e991a608316794a0bacdc9c46eeb0fa01afb Mon Sep 17 00:00:00 2001 From: "David M. Gross" Date: Mon, 12 Oct 2015 11:06:05 -0700 Subject: [PATCH 412/857] enhance Observable.fromCallable javadoc --- src/main/java/rx/Observable.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index a0d93ccacf..0a2ebab8ce 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1251,22 +1251,26 @@ public final static Observable from(T[] array) { } /** - * Returns an Observable that invokes passed function and emits its result for each new Observer that subscribes. + * Returns an Observable that, when an observer subscribes to it, invokes a function you specify and then + * emits the value returned from that function. *

- * Allows you to defer execution of passed function until Observer subscribes to the Observable. - * It makes passed function "lazy". - * Result of the function invocation will be emitted by the Observable. + * + *

+ * This allows you to defer the execution of the function you specify untl an observer subscribes to the + * Observable. That is to say, it makes the function "lazy." *

*
Scheduler:
*
{@code fromCallable} does not operate by default on a particular {@link Scheduler}.
*
* * @param func - * function which execution should be deferred, it will be invoked when Observer will subscribe to the Observable + * a function, the execution of which should be deferred; {@code fromCallable} will invoke this + * function only when an observer subscribes to the Observable that {@code fromCallable} returns * @param * the type of the item emitted by the Observable * @return an Observable whose {@link Observer}s' subscriptions trigger an invocation of the given function * @see #defer(Func0) + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental public static Observable fromCallable(Callable func) { From 01f34a77a1935b7bebe0169a1bca00dac9632b36 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 14 Oct 2015 13:24:02 +0200 Subject: [PATCH 413/857] 1.x: Completable class for valueless event composition + tests --- src/main/java/rx/Completable.java | 2181 +++++++++++ .../CompletableOnSubscribeConcat.java | 152 + .../CompletableOnSubscribeConcatArray.java | 96 + .../CompletableOnSubscribeConcatIterable.java | 135 + .../CompletableOnSubscribeMerge.java | 215 ++ .../CompletableOnSubscribeMergeArray.java | 91 + ...etableOnSubscribeMergeDelayErrorArray.java | 93 + ...bleOnSubscribeMergeDelayErrorIterable.java | 157 + .../CompletableOnSubscribeMergeIterable.java | 147 + .../CompletableOnSubscribeTimeout.java | 115 + src/test/java/rx/CompletableTest.java | 3413 +++++++++++++++++ 11 files changed, 6795 insertions(+) create mode 100644 src/main/java/rx/Completable.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeConcatArray.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeConcatIterable.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeMergeArray.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorArray.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorIterable.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeMergeIterable.java create mode 100644 src/main/java/rx/internal/operators/CompletableOnSubscribeTimeout.java create mode 100644 src/test/java/rx/CompletableTest.java diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java new file mode 100644 index 0000000000..5fd7216a3b --- /dev/null +++ b/src/main/java/rx/Completable.java @@ -0,0 +1,2181 @@ +/** + * 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; + +import java.util.Iterator; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import rx.Observable.OnSubscribe; +import rx.annotations.Experimental; +import rx.exceptions.Exceptions; +import rx.functions.*; +import rx.internal.operators.*; +import rx.internal.util.*; +import rx.plugins.*; +import rx.schedulers.Schedulers; +import rx.subscriptions.*; + +/** + * Represents a deferred computation without any value but only indication for completion or exception. + * + * The class follows a similar event pattern as Reactive-Streams: onSubscribe (onError|onComplete)? + */ +@Experimental +public class Completable { + /** + * Callback used for building deferred computations that takes a CompletableSubscriber. + */ + public interface CompletableOnSubscribe extends Action1 { + + } + + /** + * Convenience interface and callback used by the lift operator that given a child CompletableSubscriber, + * return a parent CompletableSubscriber that does any kind of lifecycle-related transformations. + */ + public interface CompletableOperator extends Func1 { + + } + + /** + * Represents the subscription API callbacks when subscribing to a Completable instance. + */ + public interface CompletableSubscriber { + /** + * Called once the deferred computation completes normally. + */ + void onCompleted(); + + /** + * Called once if the deferred computation 'throws' an exception. + * @param e the exception, not null. + */ + void onError(Throwable e); + + /** + * Called once by the Completable to set a Subscription on this instance which + * then can be used to cancel the subscription at any time. + * @param d the Subscription instance to call dispose on for cancellation, not null + */ + void onSubscribe(Subscription d); + } + + /** + * Convenience interface and callback used by the compose operator to turn a Completable into another + * Completable fluently. + */ + public interface CompletableTransformer extends Func1 { + + } + + /** Single instance of a complete Completable. */ + static final Completable COMPLETE = create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onCompleted(); + } + }); + + /** Single instance of a never Completable. */ + static final Completable NEVER = create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + s.onSubscribe(Subscriptions.unsubscribed()); + } + }); + + /** The error handler instance. */ + static final RxJavaErrorHandler ERROR_HANDLER = RxJavaPlugins.getInstance().getErrorHandler(); + + /** + * Returns a Completable which terminates as soon as one of the source Completables + * terminates (normally or with an error) and cancels all other Completables. + * @param sources the array of source Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable amb(final Completable... sources) { + requireNonNull(sources); + if (sources.length == 0) { + return complete(); + } + if (sources.length == 1) { + return sources[0]; + } + + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + s.onSubscribe(set); + + final AtomicBoolean once = new AtomicBoolean(); + + CompletableSubscriber inner = new CompletableSubscriber() { + @Override + public void onCompleted() { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onError(e); + } else { + ERROR_HANDLER.handleError(e); + } + } + + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + }; + + for (Completable c : sources) { + if (set.isUnsubscribed()) { + return; + } + if (c == null) { + NullPointerException npe = new NullPointerException("One of the sources is null"); + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onError(npe); + } else { + ERROR_HANDLER.handleError(npe); + } + return; + } + if (once.get() || set.isUnsubscribed()) { + return; + } + + // no need to have separate subscribers because inner is stateless + c.subscribe(inner); + } + } + }); + } + + /** + * Returns a Completable which terminates as soon as one of the source Completables + * terminates (normally or with an error) and cancels all other Completables. + * @param sources the array of source Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable amb(final Iterable sources) { + requireNonNull(sources); + + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + s.onSubscribe(set); + + final AtomicBoolean once = new AtomicBoolean(); + + CompletableSubscriber inner = new CompletableSubscriber() { + @Override + public void onCompleted() { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onError(e); + } else { + ERROR_HANDLER.handleError(e); + } + } + + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + }; + + Iterator it; + + try { + it = sources.iterator(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (it == null) { + s.onError(new NullPointerException("The iterator returned is null")); + return; + } + + boolean empty = true; + + for (;;) { + if (once.get() || set.isUnsubscribed()) { + return; + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable e) { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onError(e); + } else { + ERROR_HANDLER.handleError(e); + } + return; + } + + if (!b) { + if (empty) { + s.onCompleted(); + } + break; + } + + empty = false; + + if (once.get() || set.isUnsubscribed()) { + return; + } + + Completable c; + + try { + c = it.next(); + } catch (Throwable e) { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onError(e); + } else { + ERROR_HANDLER.handleError(e); + } + return; + } + + if (c == null) { + NullPointerException npe = new NullPointerException("One of the sources is null"); + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onError(npe); + } else { + ERROR_HANDLER.handleError(npe); + } + return; + } + + if (once.get() || set.isUnsubscribed()) { + return; + } + + // no need to have separate subscribers because inner is stateless + c.subscribe(inner); + } + } + }); + } + + /** + * Returns a Completable instance that completes immediately when subscribed to. + * @return a Completable instance that completes immediately + */ + public static Completable complete() { + return COMPLETE; + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + public static Completable concat(Completable... sources) { + requireNonNull(sources); + if (sources.length == 0) { + return complete(); + } else + if (sources.length == 1) { + return sources[0]; + } + return create(new CompletableOnSubscribeConcatArray(sources)); + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + public static Completable concat(Iterable sources) { + requireNonNull(sources); + + return create(new CompletableOnSubscribeConcatIterable(sources)); + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + public static Completable concat(Observable sources) { + return concat(sources, 2); + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @param prefetch the number of sources to prefetch from the sources + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + public static Completable concat(Observable sources, int prefetch) { + requireNonNull(sources); + if (prefetch < 1) { + throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); + } + return create(new CompletableOnSubscribeConcat(sources, prefetch)); + } + + /** + * Constructs a Completable instance by wrapping the given onSubscribe callback. + * @param onSubscribe the callback which will receive the CompletableSubscriber instances + * when the Completable is subscribed to. + * @return the created Completable instance + * @throws NullPointerException if onSubscribe is null + */ + public static Completable create(CompletableOnSubscribe onSubscribe) { + requireNonNull(onSubscribe); + try { + // TODO plugin wrapping onSubscribe + + return new Completable(onSubscribe); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + ERROR_HANDLER.handleError(ex); + throw toNpe(ex); + } + } + + /** + * Defers the subscription to a Completable instance returned by a supplier. + * @param completableFunc0 the supplier that returns the Completable that will be subscribed to. + * @return the Completable instance + */ + public static Completable defer(final Func0 completableFunc0) { + requireNonNull(completableFunc0); + return create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + Completable c; + + try { + c = completableFunc0.call(); + } catch (Throwable e) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(e); + return; + } + + if (c == null) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(new NullPointerException("The completable returned is null")); + return; + } + + c.subscribe(s); + } + }); + } + + /** + * Creates a Completable which calls the given error supplier for each subscriber + * and emits its returned Throwable. + *

+ * If the errorFunc0 returns null, the child CompletableSubscribers will receive a + * NullPointerException. + * @param errorFunc0 the error supplier, not null + * @return the new Completable instance + * @throws NullPointerException if errorFunc0 is null + */ + public static Completable error(final Func0 errorFunc0) { + requireNonNull(errorFunc0); + return create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + s.onSubscribe(Subscriptions.unsubscribed()); + Throwable error; + + try { + error = errorFunc0.call(); + } catch (Throwable e) { + error = e; + } + + if (error == null) { + error = new NullPointerException("The error supplied is null"); + } + s.onError(error); + } + }); + } + + /** + * Creates a Completable instance that emits the given Throwable exception to subscribers. + * @param error the Throwable instance to emit, not null + * @return the new Completable instance + * @throws NullPointerException if error is null + */ + public static Completable error(final Throwable error) { + requireNonNull(error); + return create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(error); + } + }); + } + + /** + * Returns a Completable instance that runs the given Action0 for each subscriber and + * emits either an unchecked exception or simply completes. + * @param run the runnable to run for each subscriber + * @return the new Completable instance + * @throws NullPointerException if run is null + */ + public static Completable fromAction(final Action0 action) { + requireNonNull(action); + return create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + try { + action.call(); + } catch (Throwable e) { + if (!bs.isUnsubscribed()) { + s.onError(e); + } + return; + } + if (!bs.isUnsubscribed()) { + s.onCompleted(); + } + } + }); + } + + /** + * Returns a Completable which when subscribed, executes the callable function, ignores its + * normal result and emits onError or onCompleted only. + * @param callable the callable instance to execute for each subscriber + * @return the new Completable instance + */ + public static Completable fromCallable(final Callable callable) { + requireNonNull(callable); + return create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + try { + callable.call(); + } catch (Throwable e) { + if (!bs.isUnsubscribed()) { + s.onError(e); + } + return; + } + if (!bs.isUnsubscribed()) { + s.onCompleted(); + } + } + }); + } + + /** + * Returns a Completable instance that reacts to the termination of the given Future in a blocking fashion. + *

+ * Note that cancellation from any of the subscribers to this Completable will cancel the future. + * @param future the future to react to + * @return the new Completable instance + */ + public static Completable fromFuture(Future future) { + requireNonNull(future); + return fromObservable(Observable.from(future)); + } + + /** + * Returns a Completable instance that subscribes to the given flowable, ignores all values and + * emits only the terminal event. + * @param flowable the Flowable instance to subscribe to, not null + * @return the new Completable instance + * @throws NullPointerException if flowable is null + */ + public static Completable fromObservable(final Observable flowable) { + requireNonNull(flowable); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber cs) { + Subscriber subscriber = new Subscriber() { + + @Override + public void onCompleted() { + cs.onCompleted(); + } + + @Override + public void onError(Throwable t) { + cs.onError(t); + } + + @Override + public void onNext(Object t) { + // ignored + } + }; + cs.onSubscribe(subscriber); + flowable.subscribe(subscriber); + } + }); + } + + /** + * Returns a Completable instance that when subscribed to, subscribes to the Single instance and + * emits a completion event if the single emits onSuccess or forwards any onError events. + * @param single the Single instance to subscribe to, not null + * @return the new Completable instance + * @throws NullPointerException if single is null + */ + public static Completable fromSingle(final Single single) { + requireNonNull(single); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + SingleSubscriber te = new SingleSubscriber() { + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSuccess(Object value) { + s.onCompleted(); + } + + }; + s.onSubscribe(te); + single.subscribe(te); + } + }); + } + + /** + * Returns a Completable instance that subscribes to all sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable merge(Completable... sources) { + requireNonNull(sources); + if (sources.length == 0) { + return complete(); + } else + if (sources.length == 1) { + return sources[0]; + } + return create(new CompletableOnSubscribeMergeArray(sources)); + } + + /** + * Returns a Completable instance that subscribes to all sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable merge(Iterable sources) { + requireNonNull(sources); + return create(new CompletableOnSubscribeMergeIterable(sources)); + } + + /** + * Returns a Completable instance that subscribes to all sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable merge(Observable sources) { + return merge0(sources, Integer.MAX_VALUE, false); + } + + /** + * Returns a Completable instance that keeps subscriptions to a limited number of sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @param maxConcurrency the maximum number of concurrent subscriptions + * @return the new Completable instance + * @throws NullPointerException if sources is null + * @throws IllegalArgumentException if maxConcurrency is less than 1 + */ + public static Completable merge(Observable sources, int maxConcurrency) { + return merge0(sources, maxConcurrency, false); + + } + + /** + * Returns a Completable instance that keeps subscriptions to a limited number of sources at once and + * completes only when all source Completables terminate in one way or another, combining any exceptions + * thrown by either the sources Observable or the inner Completable instances. + * @param sources the iterable sequence of sources. + * @param maxConcurrency the maximum number of concurrent subscriptions + * @param delayErrors delay all errors from the main source and from the inner Completables? + * @return the new Completable instance + * @throws NullPointerException if sources is null + * @throws IllegalArgumentException if maxConcurrency is less than 1 + */ + protected static Completable merge0(Observable sources, int maxConcurrency, boolean delayErrors) { + requireNonNull(sources); + if (maxConcurrency < 1) { + throw new IllegalArgumentException("maxConcurrency > 0 required but it was " + maxConcurrency); + } + return create(new CompletableOnSubscribeMerge(sources, maxConcurrency, delayErrors)); + } + + /** + * Returns a Completable that subscribes to all Completables in the source array and delays + * any error emitted by either the sources observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the array of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Completable... sources) { + requireNonNull(sources); + return create(new CompletableOnSubscribeMergeDelayErrorArray(sources)); + } + + /** + * Returns a Completable that subscribes to all Completables in the source sequence and delays + * any error emitted by either the sources observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the sequence of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Iterable sources) { + requireNonNull(sources); + return create(new CompletableOnSubscribeMergeDelayErrorIterable(sources)); + } + + /** + * Returns a Completable that subscribes to all Completables in the source sequence and delays + * any error emitted by either the sources observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the sequence of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Observable sources) { + return merge0(sources, Integer.MAX_VALUE, true); + } + + + /** + * Returns a Completable that subscribes to a limited number of inner Completables at once in + * the source sequence and delays any error emitted by either the sources + * observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the sequence of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Observable sources, int maxConcurrency) { + return merge0(sources, maxConcurrency, true); + } + + /** + * Returns a Completable that never calls onError or onComplete. + * @return the singleton instance that never calls onError or onComplete + */ + public static Completable never() { + return NEVER; + } + + /** + * Java 7 backport: throws a NullPointerException if o is null. + * @param o the object to check + * @return the o value + * @throws NullPointerException if o is null + */ + static T requireNonNull(T o) { + if (o == null) { + throw new NullPointerException(); + } + return o; + } + + /** + * Returns a Completable instance that fires its onComplete event after the given delay ellapsed. + * @param delay the delay time + * @param unit the delay unit + * @return the new Completable instance + */ + public static Completable timer(long delay, TimeUnit unit) { + return timer(delay, unit, Schedulers.computation()); + } + + /** + * Returns a Completable instance that fires its onComplete event after the given delay ellapsed + * by using the supplied scheduler. + * @param delay the delay time + * @param unit the delay unit + * @return the new Completable instance + */ + public static Completable timer(final long delay, final TimeUnit unit, final Scheduler scheduler) { + requireNonNull(unit); + requireNonNull(scheduler); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + MultipleAssignmentSubscription mad = new MultipleAssignmentSubscription(); + s.onSubscribe(mad); + if (!mad.isUnsubscribed()) { + final Scheduler.Worker w = scheduler.createWorker(); + mad.set(w); + w.schedule(new Action0() { + @Override + public void call() { + try { + s.onCompleted(); + } finally { + w.unsubscribe(); + } + } + }, delay, unit); + } + } + }); + } + + /** + * Creates a NullPointerException instance and sets the given Throwable as its initial cause. + * @param ex the Throwable instance to use as cause, not null (not verified) + * @return the created NullPointerException + */ + static NullPointerException toNpe(Throwable ex) { + NullPointerException npe = new NullPointerException("Actually not, but can't pass out an exception otherwise..."); + npe.initCause(ex); + return npe; + } + + /** + * Returns a Completable instance which manages a resource along + * with a custom Completable instance while the subscription is active. + *

+ * This overload performs an eager unsubscription before the terminal event is emitted. + * + * @param resourceFunc0 the supplier that returns a resource to be managed. + * @param completableFunc1 the function that given a resource returns a Completable instance that will be subscribed to + * @param disposer the consumer that disposes the resource created by the resource supplier + * @return the new Completable instance + */ + public static Completable using(Func0 resourceFunc0, + Func1 completableFunc1, + Action1 disposer) { + return using(resourceFunc0, completableFunc1, disposer, true); + } + + /** + * Returns a Completable instance which manages a resource along + * with a custom Completable instance while the subscription is active and performs eager or lazy + * resource disposition. + *

+ * If this overload performs a lazy unsubscription after the terminal event is emitted. + * Exceptions thrown at this time will be delivered to RxJavaPlugins only. + * + * @param resourceFunc0 the supplier that returns a resource to be managed + * @param completableFunc1 the function that given a resource returns a non-null + * Completable instance that will be subscribed to + * @param disposer the consumer that disposes the resource created by the resource supplier + * @param eager if true, the resource is disposed before the terminal event is emitted, if false, the + * resource is disposed after the terminal event has been emitted + * @return the new Completable instance + */ + public static Completable using(final Func0 resourceFunc0, + final Func1 completableFunc1, + final Action1 disposer, + final boolean eager) { + requireNonNull(resourceFunc0); + requireNonNull(completableFunc1); + requireNonNull(disposer); + + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + final R resource; + + try { + resource = resourceFunc0.call(); + } catch (Throwable e) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(e); + return; + } + + Completable cs; + + try { + cs = completableFunc1.call(resource); + } catch (Throwable e) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(e); + return; + } + + if (cs == null) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(new NullPointerException("The completable supplied is null")); + return; + } + + final AtomicBoolean once = new AtomicBoolean(); + + cs.subscribe(new CompletableSubscriber() { + Subscription d; + void dispose() { + d.unsubscribe(); + if (once.compareAndSet(false, true)) { + try { + disposer.call(resource); + } catch (Throwable ex) { + ERROR_HANDLER.handleError(ex); + } + } + } + + @Override + public void onCompleted() { + if (eager) { + if (once.compareAndSet(false, true)) { + try { + disposer.call(resource); + } catch (Throwable ex) { + s.onError(ex); + return; + } + } + } + + s.onCompleted(); + + if (!eager) { + dispose(); + } + } + + @Override + public void onError(Throwable e) { + if (eager) { + if (once.compareAndSet(false, true)) { + try { + disposer.call(resource); + } catch (Throwable ex) { + ex.addSuppressed(e); + e = ex; + } + } + } + + s.onError(e); + + if (!eager) { + dispose(); + } + } + + @Override + public void onSubscribe(Subscription d) { + this.d = d; + s.onSubscribe(Subscriptions.create(new Action0() { + @Override + public void call() { + dispose(); + } + })); + } + }); + } + }); + } + + /** The actual subscription action. */ + private final CompletableOnSubscribe onSubscribe; + + /** + * Constructs a Completable instance with the given onSubscribe callback. + * @param onSubscribe the callback that will receive CompletableSubscribers when they subscribe, + * not null (not verified) + */ + protected Completable(CompletableOnSubscribe onSubscribe) { + this.onSubscribe = onSubscribe; + } + + /** + * Returns a Completable that emits the a terminated event of either this Completable + * or the other Completable whichever fires first. + * @param other the other Completable, not null + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + public final Completable ambWith(Completable other) { + requireNonNull(other); + return amb(this, other); + } + + /** + * Subscribes to and awaits the termination of this Completable instance in a blocking manner and + * rethrows any exception emitted. + * @throws RuntimeException wrapping an InterruptedException if the current thread is interrupted + */ + public final void await() { + final CountDownLatch cdl = new CountDownLatch(1); + final Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Subscription d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + return; + } + try { + cdl.await(); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + } + + /** + * Subscribes to and awaits the termination of this Completable instance in a blocking manner + * with a specific timeout and rethrows any exception emitted within the timeout window. + * @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. + * @throws RuntimeException wrapping an InterruptedException if the current thread is interrupted + */ + public final boolean await(long timeout, TimeUnit unit) { + requireNonNull(unit); + + final CountDownLatch cdl = new CountDownLatch(1); + final Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Subscription d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + return true; + } + boolean b; + try { + b = cdl.await(timeout, unit); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + if (b) { + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + } + return b; + } + + /** + * Calls the given transformer function with this instance and returns the function's resulting + * Completable. + * @param transformer the transformer function, not null + * @return the Completable returned by the function + * @throws NullPointerException if transformer is null + */ + public final Completable compose(CompletableTransformer transformer) { + return to(transformer); + } + + /** + * Concatenates this Completable with another Completable. + * @param other the other Completable, not null + * @return the new Completable which subscribes to this and then the other Completable + * @throws NullPointerException if other is null + */ + public final Completable concatWith(Completable other) { + requireNonNull(other); + return concat(this, other); + } + + /** + * Returns a Completable which delays the emission of the completion event by the given time. + * @param delay the delay time + * @param unit the delay unit + * @return the new Completable instance + * @throws NullPointerException if unit is null + */ + public final Completable delay(long delay, TimeUnit unit) { + return delay(delay, unit, Schedulers.computation(), false); + } + + /** + * Returns a Completable which delays the emission of the completion event by the given time while + * running on the specified scheduler. + * @param delay the delay time + * @param unit the delay unit + * @param scheduler the scheduler to run the delayed completion on + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler is null + */ + public final Completable delay(long delay, TimeUnit unit, Scheduler scheduler) { + return delay(delay, unit, scheduler, false); + } + + /** + * Returns a Completable which delays the emission of the completion event, and optionally the error as well, by the given time while + * running on the specified scheduler. + * @param delay the delay time + * @param unit the delay unit + * @param scheduler the scheduler to run the delayed completion on + * @param delayError delay the error emission as well? + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler is null + */ + public final Completable delay(final long delay, final TimeUnit unit, final Scheduler scheduler, final boolean delayError) { + requireNonNull(unit); + requireNonNull(scheduler); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + + final Scheduler.Worker w = scheduler.createWorker(); + set.add(w); + + subscribe(new CompletableSubscriber() { + + + @Override + public void onCompleted() { + set.add(w.schedule(new Action0() { + @Override + public void call() { + try { + s.onCompleted(); + } finally { + w.unsubscribe(); + } + } + }, delay, unit)); + } + + @Override + public void onError(final Throwable e) { + if (delayError) { + set.add(w.schedule(new Action0() { + @Override + public void call() { + try { + s.onError(e); + } finally { + w.unsubscribe(); + } + } + }, delay, unit)); + } else { + s.onError(e); + } + } + + @Override + public void onSubscribe(Subscription d) { + set.add(d); + s.onSubscribe(set); + } + + }); + } + }); + } + + /** + * Returns a Completable which calls the given onComplete callback if this Completable completes. + * @param onComplete the callback to call when this emits an onComplete event + * @return the new Completable instance + * @throws NullPointerException if onComplete is null + */ + public final Completable doOnComplete(Action0 onComplete) { + return doOnLifecycle(Actions.empty(), Actions.empty(), onComplete, Actions.empty(), Actions.empty()); + } + + /** + * Returns a Completable which calls the giveon 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 + * @throws NullPointerException if onDispose is null + */ + public final Completable doOnUnsubscribe(Action0 onUnsubscribe) { + return doOnLifecycle(Actions.empty(), Actions.empty(), Actions.empty(), Actions.empty(), onUnsubscribe); + } + + /** + * Returns a Completable which calls the given onError callback if this Completable emits an error. + * @param onError the error callback + * @return the new Completable instance + * @throws NullPointerException if onError is null + */ + public final Completable doOnError(Action1 onError) { + return doOnLifecycle(Actions.empty(), onError, Actions.empty(), Actions.empty(), Actions.empty()); + } + + /** + * Returns a Completable instance that calls the various callbacks on the specific + * lifecycle events. + * @param onSubscribe the consumer called when a CompletableSubscriber subscribes. + * @param onError the consumer called when this emits an onError event + * @param onComplete the runnable called just before when this Completable completes normally + * @param onAfterComplete the runnable called after this Completable completes normally + * @param onUnsubscribe the runnable called when the child cancels the subscription + * @return the new Completable instance + */ + protected final Completable doOnLifecycle( + final Action1 onSubscribe, + final Action1 onError, + final Action0 onComplete, + final Action0 onAfterComplete, + final Action0 onUnsubscribe) { + requireNonNull(onSubscribe); + requireNonNull(onError); + requireNonNull(onComplete); + requireNonNull(onAfterComplete); + requireNonNull(onUnsubscribe); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + try { + onComplete.call(); + } catch (Throwable e) { + s.onError(e); + return; + } + + s.onCompleted(); + + try { + onAfterComplete.call(); + } catch (Throwable e) { + ERROR_HANDLER.handleError(e); + } + } + + @Override + public void onError(Throwable e) { + try { + onError.call(e); + } catch (Throwable ex) { + ex.addSuppressed(e); + e = ex; + } + + s.onError(e); + } + + @Override + public void onSubscribe(final Subscription d) { + + try { + onSubscribe.call(d); + } catch (Throwable ex) { + d.unsubscribe(); + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(ex); + return; + } + + s.onSubscribe(Subscriptions.create(new Action0() { + @Override + public void call() { + try { + onUnsubscribe.call(); + } catch (Throwable e) { + ERROR_HANDLER.handleError(e); + } + d.unsubscribe(); + } + })); + } + + }); + } + }); + } + + /** + * Returns a Completable instance that calls the given onSubscribe callback with the disposable + * that child subscribers receive on subscription. + * @param onSubscribe the callback called when a child subscriber subscribes + * @return the new Completable instance + * @throws NullPointerException if onSubscribe is null + */ + public final Completable doOnSubscribe(Action1 onSubscribe) { + return doOnLifecycle(onSubscribe, Actions.empty(), Actions.empty(), Actions.empty(), Actions.empty()); + } + + /** + * Returns a Completable instance that calls the given onTerminate callback just before this Completable + * completes normally or with an exception + * @param onTerminate the callback to call just before this Completable terminates + * @return the new Completable instance + */ + public final Completable doOnTerminate(final Action0 onTerminate) { + return doOnLifecycle(Actions.empty(), new Action1() { + @Override + public void call(Throwable e) { + onTerminate.call(); + } + }, onTerminate, Actions.empty(), Actions.empty()); + } + + /** + * Returns a completable that first runs this Completable + * and then the other completable. + *

+ * This is an alias for {@link #concatWith(Completable)}. + * @param other the other Completable, not null + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + public final Completable endWith(Completable other) { + return concatWith(other); + } + + /** + * Returns an Observable that first runs this Completable instance and + * resumes with the given next Observable. + * @param next the next Observable to continue + * @return the new Observable instance + * @throws NullPointerException if next is null + */ + public final Observable endWith(Observable next) { + return next.startWith(this.toObservable()); + } + + /** + * Returns a Completable instace 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 + * @throws NullPointerException if onAfterComplete is null + */ + public final Completable finallyDo(Action0 onAfterComplete) { + return doOnLifecycle(Actions.empty(), Actions.empty(), Actions.empty(), onAfterComplete, Actions.empty()); + } + + /** + * Subscribes to this Completable instance and blocks until it terminates, then returns null 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 + */ + public final Throwable get() { + final CountDownLatch cdl = new CountDownLatch(1); + final Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Subscription d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + return err[0]; + } + try { + cdl.await(); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + return err[0]; + } + + /** + * 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. + * @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 + */ + public final Throwable get(long timeout, TimeUnit unit) { + requireNonNull(unit); + + final CountDownLatch cdl = new CountDownLatch(1); + final Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Subscription d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + return err[0]; + } + boolean b; + try { + b = cdl.await(timeout, unit); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + if (b) { + return err[0]; + } + Exceptions.propagate(new TimeoutException()); + return null; + } + + /** + * Lifts a CompletableSubscriber transformation into the chain of Completables. + * @param onLift the lifting function that transforms the child subscriber with a parent subscriber. + * @return the new Completable instance + * @throws NullPointerException if onLift is null + */ + public final Completable lift(final CompletableOperator onLift) { + requireNonNull(onLift); + return create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + try { + // TODO plugin wrapping + + CompletableSubscriber sw = onLift.call(s); + + subscribe(sw); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + throw toNpe(ex); + } + } + }); + } + + /** + * Returns a Completable which subscribes to this and the other Completable and completes + * when both of them complete or one emits an error. + * @param other the other Completable instance + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + public final Completable mergeWith(Completable other) { + requireNonNull(other); + return merge(this, other); + } + + /** + * Returns a Completable which emits the terminal events from the thread of the specified scheduler. + * @param scheduler the scheduler to emit terminal events on + * @return the new Completable instance + * @throws NullPointerException if scheduler is null + */ + public final Completable observeOn(final Scheduler scheduler) { + requireNonNull(scheduler); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + + final SubscriptionList ad = new SubscriptionList(); + + final Scheduler.Worker w = scheduler.createWorker(); + ad.add(w); + + s.onSubscribe(ad); + + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + w.schedule(new Action0() { + @Override + public void call() { + try { + s.onCompleted(); + } finally { + ad.unsubscribe(); + } + } + }); + } + + @Override + public void onError(final Throwable e) { + w.schedule(new Action0() { + @Override + public void call() { + try { + s.onError(e); + } finally { + ad.unsubscribe(); + } + } + }); + } + + @Override + public void onSubscribe(Subscription d) { + ad.add(d); + } + + }); + } + }); + } + + /** + * Returns a Completable instance that if this Completable emits an error, it will emit an onComplete + * and swallow the throwable. + * @return the new Completable instance + */ + public final Completable onErrorComplete() { + return onErrorComplete(UtilityFunctions.alwaysTrue()); + } + + /** + * Returns a Completable instance that if this Completable emits an error and the predicate returns + * true, it will emit an onComplete and swallow the throwable. + * @param predicate the predicate to call when an Throwable is emitted which should return true + * if the Throwable should be swallowed and replaced with an onComplete. + * @return the new Completable instance + */ + public final Completable onErrorComplete(final Func1 predicate) { + requireNonNull(predicate); + + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + boolean b; + + try { + b = predicate.call(e); + } catch (Throwable ex) { + e.addSuppressed(ex); + s.onError(e); + return; + } + + if (b) { + s.onCompleted(); + } else { + s.onError(e); + } + } + + @Override + public void onSubscribe(Subscription d) { + s.onSubscribe(d); + } + + }); + } + }); + } + + /** + * Returns a Completable instance that when encounters an error from this Completable, calls the + * specified mapper function that returns another Completable instance for it and resumes the + * execution with it. + * @param errorMapper the mapper function that takes the error and should return a Completable as + * continuation. + * @return the new Completable instance + */ + public final Completable onErrorResumeNext(final Func1 errorMapper) { + requireNonNull(errorMapper); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + final SerialSubscription sd = new SerialSubscription(); + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + Completable c; + + try { + c = errorMapper.call(e); + } catch (Throwable ex) { + ex.addSuppressed(e); + s.onError(ex); + return; + } + + if (c == null) { + NullPointerException npe = new NullPointerException("The completable returned is null"); + npe.addSuppressed(e); + s.onError(npe); + return; + } + + c.subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(Subscription d) { + sd.set(d); + } + + }); + } + + @Override + public void onSubscribe(Subscription d) { + sd.set(d); + } + + }); + } + }); + } + + /** + * Returns a Completable that repeatedly subscribes to this Completable until cancelled. + * @return the new Completable instance + */ + public final Completable repeat() { + return fromObservable(toObservable().repeat()); + } + + /** + * Returns a Completable that subscribes repeatedly at most the given times to this Completable. + * @param times the number of times the resubscription should happen + * @return the new Completable instance + * @throws IllegalArgumentException if times is less than zero + */ + public final Completable repeat(long times) { + return fromObservable(toObservable().repeat(times)); + } + + /** + * Returns a Completable instance that repeats when the Publisher returned by the handler + * emits an item or completes when this Publisher emits a completed event. + * @param handler the function that transforms the stream of values indicating the completion of + * this Completable and returns a Publisher that emits items for repeating or completes to indicate the + * repetition should stop + * @return the new Completable instance + * @throws NullPointerException if stop is null + */ + public final Completable repeatWhen(Func1, ? extends Observable> handler) { + requireNonNull(handler); // FIXME do a null check in Observable + return fromObservable(toObservable().repeatWhen(handler)); + } + + /** + * Returns a Completable that retries this Completable as long as it emits an onError event. + * @return the new Completable instance + */ + public final Completable retry() { + return fromObservable(toObservable().retry()); + } + + /** + * Returns a Completable that retries this Completable in case of an error as long as the predicate + * returns true. + * @param predicate the predicate called when this emits an error with the repeat count and the latest exception + * and should return true to retry. + * @return the new Completable instance + */ + public final Completable retry(Func2 predicate) { + return fromObservable(toObservable().retry(predicate)); + } + + /** + * Returns a Completable that when this Completable emits an error, retries at most the given + * number of times before giving up and emitting the last error. + * @param times the number of times the returned Completable should retry this Completable + * @return the new Completable instance + * @throws IllegalArgumentException if times is negative + */ + public final Completable retry(long times) { + return fromObservable(toObservable().retry(times)); + } + + /** + * Returns a Completable which given a Publisher and when this Completable emits an error, delivers + * that error through an Observable and the Publisher should return a value indicating a retry in response + * or a terminal event indicating a termination. + * @param handler the handler that receives an Observable delivering Throwables and should return a Publisher that + * emits items to indicate retries or emits terminal events to indicate termination. + * @return the new Completable instance + * @throws NullPointerException if handler is null + */ + public final Completable retryWhen(Func1, ? extends Observable> handler) { + return fromObservable(toObservable().retryWhen(handler)); + } + + /** + * Returns a Completable which first runs the other Completable + * then this completable if the other completed normally. + * @param other the other completable to run first + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + public final Completable startWith(Completable other) { + requireNonNull(other); + return concat(other, this); + } + + /** + * Returns an Observable which first delivers the events + * of the other Observable then runs this Completable. + * @param other the other Observable to run first + * @return the new Observable instance + * @throws NullPointerException if other is null + */ + public final Observable startWith(Observable other) { + requireNonNull(other); + return this.toObservable().startWith(other); + } + + /** + * Subscribes to this Completable and returns a Subscription which can be used to cancel + * the subscription. + * @return the Subscription that allows cancelling the subscription + */ + public final Subscription subscribe() { + final MultipleAssignmentSubscription mad = new MultipleAssignmentSubscription(); + subscribe(new CompletableSubscriber() { + @Override + public void onCompleted() { + // nothing to do + } + + @Override + public void onError(Throwable e) { + ERROR_HANDLER.handleError(e); + } + + @Override + public void onSubscribe(Subscription d) { + mad.set(d); + } + }); + + return mad; + } + /** + * Subscribes to this Completable and calls the given Action0 when this Completable + * completes normally. + *

+ * If this Completable emits an error, it is sent to ERROR_HANDLER.handleError and gets swallowed. + * @param onComplete the runnable called when this Completable completes normally + * @return the Subscription that allows cancelling the subscription + */ + public final Subscription subscribe(final Action0 onComplete) { + requireNonNull(onComplete); + + final MultipleAssignmentSubscription mad = new MultipleAssignmentSubscription(); + subscribe(new CompletableSubscriber() { + @Override + public void onCompleted() { + try { + onComplete.call(); + } catch (Throwable e) { + ERROR_HANDLER.handleError(e); + } + } + + @Override + public void onError(Throwable e) { + ERROR_HANDLER.handleError(e); + } + + @Override + public void onSubscribe(Subscription d) { + mad.set(d); + } + }); + + return mad; + } + + /** + * Subscribes to this Completable and calls back either the onError or onComplete functions. + * + * @param onError the consumer that is called if this Completable emits an error + * @param onComplete the runnable that is called if the Completable completes normally + * @return the Subscription that can be used for cancelling the subscription asynchronously + * @throws NullPointerException if either callback is null + */ + public final Subscription subscribe(final Action1 onError, final Action0 onComplete) { + requireNonNull(onError); + requireNonNull(onComplete); + + final MultipleAssignmentSubscription mad = new MultipleAssignmentSubscription(); + subscribe(new CompletableSubscriber() { + @Override + public void onCompleted() { + try { + onComplete.call(); + } catch (Throwable e) { + onError(e); + } + } + + @Override + public void onError(Throwable e) { + try { + onError.call(e); + } catch (Throwable ex) { + e.addSuppressed(ex); + ERROR_HANDLER.handleError(e); + } + } + + @Override + public void onSubscribe(Subscription d) { + mad.set(d); + } + }); + + return mad; + } + + /** + * Subscribes the given CompletableSubscriber to this Completable instance. + * @param s the CompletableSubscriber, not null + * @throws NullPointerException if s is null + */ + public final void subscribe(CompletableSubscriber s) { + requireNonNull(s); + try { + // TODO plugin wrapping the subscriber + + onSubscribe.call(s); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + ERROR_HANDLER.handleError(ex); + throw toNpe(ex); + } + } + + /** + * Subscribes a reactive-streams Subscriber to this Completable instance which + * will receive only an onError or onComplete event. + * @param s the reactive-streams Subscriber, not null + * @throws NullPointerException if s is null + */ + public final void subscribe(Subscriber s) { + requireNonNull(s); + try { + final Subscriber sw = s; // FIXME hooking in 1.x is kind of strange to me + + if (sw == null) { + throw new NullPointerException("The RxJavaPlugins.onSubscribe returned a null Subscriber"); + } + + subscribe(new CompletableSubscriber() { + @Override + public void onCompleted() { + sw.onCompleted(); + } + + @Override + public void onError(Throwable e) { + sw.onError(e); + } + + @Override + public void onSubscribe(Subscription d) { + sw.add(d); + } + }); + + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + ERROR_HANDLER.handleError(ex); + throw toNpe(ex); + } + } + + /** + * Returns a Completable which subscribes the child subscriber on the specified scheduler, making + * sure the subscription side-effects happen on that specific thread of the scheduler. + * @param scheduler the Scheduler to subscribe on + * @return the new Completable instance + * @throws NullPointerException if scheduler is null + */ + public final Completable subscribeOn(final Scheduler scheduler) { + requireNonNull(scheduler); + + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + // FIXME cancellation of this schedule + + final Scheduler.Worker w = scheduler.createWorker(); + + w.schedule(new Action0() { + @Override + public void call() { + try { + subscribe(s); + } finally { + w.unsubscribe(); + } + } + }); + } + }); + } + + /** + * Returns a Completable that runs this Completable and emits a TimeoutException in case + * this Completable doesn't complete within the given time. + * @param timeout the timeout value + * @param unit the timeout unit + * @return the new Completable instance + * @throws NullPointerException if unit is null + */ + public final Completable timeout(long timeout, TimeUnit unit) { + return timeout0(timeout, unit, Schedulers.computation(), null); + } + + /** + * Returns a Completable that runs this Completable and switches to the other Completable + * in case this Completable doesn't complete within the given time. + * @param timeout the timeout value + * @param unit the timeout unit + * @param other the other Completable instance to switch to in case of a timeout + * @return the new Completable instance + * @throws NullPointerException if unit or other is null + */ + public final Completable timeout(long timeout, TimeUnit unit, Completable other) { + requireNonNull(other); + return timeout0(timeout, unit, Schedulers.computation(), other); + } + + /** + * Returns a Completable that runs this Completable and emits a TimeoutException in case + * this Completable doesn't complete within the given time while "waiting" on the specified + * Scheduler. + * @param timeout the timeout value + * @param unit the timeout unit + * @param scheduler the scheduler to use to wait for completion + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler is null + */ + public final Completable timeout(long timeout, TimeUnit unit, Scheduler scheduler) { + return timeout0(timeout, unit, scheduler, null); + } + + /** + * Returns a Completable that runs this Completable and switches to the other Completable + * in case this Completable doesn't complete within the given time while "waiting" on + * the specified scheduler. + * @param timeout the timeout value + * @param unit the timeout unit + * @param scheduler the scheduler to use to wait for completion + * @param other the other Completable instance to switch to in case of a timeout + * @return the new Completable instance + * @throws NullPointerException if unit, scheduler or other is null + */ + public final Completable timeout(long timeout, TimeUnit unit, Scheduler scheduler, Completable other) { + requireNonNull(other); + return timeout0(timeout, unit, scheduler, other); + } + + /** + * Returns a Completable that runs this Completable and optionally switches to the other Completable + * in case this Completable doesn't complete within the given time while "waiting" on + * the specified scheduler. + * @param timeout the timeout value + * @param unit the timeout unit + * @param scheduler the scheduler to use to wait for completion + * @param other the other Completable instance to switch to in case of a timeout, + * if null a TimeoutException is emitted instead + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler + */ + public final Completable timeout0(long timeout, TimeUnit unit, Scheduler scheduler, Completable other) { + requireNonNull(unit); + requireNonNull(scheduler); + return create(new CompletableOnSubscribeTimeout(this, timeout, unit, scheduler, other)); + } + + /** + * Allows fluent conversion to another type via a function callback. + * @param converter the function called with this which should return some other value. + * @return the converted value + * @throws NullPointerException if converter is null + */ + public final U to(Func1 converter) { + return converter.call(this); + } + + /** + * Returns an Observable which when subscribed to subscribes to this Completable and + * relays the terminal events to the subscriber. + * @return the new Observable created + */ + public final Observable toObservable() { + return Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber s) { + subscribe(s); + } + }); + } + + /** + * Convers 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 + * @throws NullPointerException if completionValueFunc0 is null + */ + public final Single toSingle(final Func0 completionValueFunc0) { + requireNonNull(completionValueFunc0); + return Single.create(new rx.Single.OnSubscribe() { + @Override + public void call(final SingleSubscriber s) { + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + T v; + + try { + v = completionValueFunc0.call(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (v == null) { + s.onError(new NullPointerException("The value supplied is null")); + } else { + s.onSuccess(v); + } + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(Subscription d) { + s.add(d); + } + + }); + } + }); + } + + /** + * Convers 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 + * @throws NullPointerException if completionValue is null + */ + public final Single toSingleDefault(final T completionValue) { + requireNonNull(completionValue); + return toSingle(new Func0() { + @Override + public T call() { + return completionValue; + } + }); + } + + /** + * Returns a Completable which makes sure when a subscriber cancels the subscription, the + * dispose is called on the specified scheduler + * @param scheduler the target scheduler where to execute the cancellation + * @return the new Completable instance + * @throws NullPointerException if scheduler is null + */ + public final Completable unsubscribeOn(final Scheduler scheduler) { + requireNonNull(scheduler); + return create(new CompletableOnSubscribe() { + @Override + public void call(final CompletableSubscriber s) { + subscribe(new CompletableSubscriber() { + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(final Subscription d) { + s.onSubscribe(Subscriptions.create(new Action0() { + @Override + public void call() { + final Scheduler.Worker w = scheduler.createWorker(); + w.schedule(new Action0() { + @Override + public void call() { + try { + d.unsubscribe(); + } finally { + w.unsubscribe(); + } + } + }); + } + })); + } + + }); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java new file mode 100644 index 0000000000..c7da20df07 --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcat.java @@ -0,0 +1,152 @@ +/** + * 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.atomic.*; + +import rx.*; +import rx.Completable.*; +import rx.exceptions.MissingBackpressureException; +import rx.internal.util.unsafe.SpscArrayQueue; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.SerialSubscription; + +public final class CompletableOnSubscribeConcat implements CompletableOnSubscribe { + final Observable sources; + final int prefetch; + + public CompletableOnSubscribeConcat(Observable sources, int prefetch) { + this.sources = sources; + this.prefetch = prefetch; + } + + @Override + public void call(CompletableSubscriber s) { + CompletableConcatSubscriber parent = new CompletableConcatSubscriber(s, prefetch); + s.onSubscribe(parent); + sources.subscribe(parent); + } + + static final class CompletableConcatSubscriber + extends Subscriber { + final CompletableSubscriber actual; + final int prefetch; + final SerialSubscription sr; + + final SpscArrayQueue queue; + + volatile boolean done; + + volatile int once; + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(CompletableConcatSubscriber.class, "once"); + + final ConcatInnerSubscriber inner; + + final AtomicInteger wip; + + public CompletableConcatSubscriber(CompletableSubscriber actual, int prefetch) { + this.actual = actual; + this.prefetch = prefetch; + this.queue = new SpscArrayQueue(prefetch); + this.sr = new SerialSubscription(); + this.inner = new ConcatInnerSubscriber(); + this.wip = new AtomicInteger(); + add(sr); + request(prefetch); + } + + @Override + public void onNext(Completable t) { + if (!queue.offer(t)) { + onError(new MissingBackpressureException()); + return; + } + if (wip.getAndIncrement() == 0) { + next(); + } + } + + @Override + public void onError(Throwable t) { + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onError(t); + return; + } + RxJavaPlugins.getInstance().getErrorHandler().handleError(t); + } + + @Override + public void onCompleted() { + if (done) { + return; + } + done = true; + if (wip.getAndIncrement() == 0) { + next(); + } + } + + void innerError(Throwable e) { + unsubscribe(); + onError(e); + } + + void innerComplete() { + if (wip.decrementAndGet() != 0) { + next(); + } + if (!done) { + request(1); + } + } + + void next() { + boolean d = done; + Completable c = queue.poll(); + if (c == null) { + if (d) { + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onCompleted(); + } + return; + } + RxJavaPlugins.getInstance().getErrorHandler().handleError(new IllegalStateException("Queue is empty?!")); + return; + } + + c.subscribe(inner); + } + + final class ConcatInnerSubscriber implements CompletableSubscriber { + @Override + public void onSubscribe(Subscription d) { + sr.set(d); + } + + @Override + public void onError(Throwable e) { + innerError(e); + } + + @Override + public void onCompleted() { + innerComplete(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeConcatArray.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcatArray.java new file mode 100644 index 0000000000..c1f48f61b7 --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcatArray.java @@ -0,0 +1,96 @@ +/** + * 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.atomic.AtomicInteger; + +import rx.*; +import rx.Completable.*; +import rx.subscriptions.SerialSubscription; + +public final class CompletableOnSubscribeConcatArray implements CompletableOnSubscribe { + final Completable[] sources; + + public CompletableOnSubscribeConcatArray(Completable[] sources) { + this.sources = sources; + } + + @Override + public void call(CompletableSubscriber s) { + ConcatInnerSubscriber inner = new ConcatInnerSubscriber(s, sources); + s.onSubscribe(inner.sd); + inner.next(); + } + + static final class ConcatInnerSubscriber extends AtomicInteger implements CompletableSubscriber { + /** */ + private static final long serialVersionUID = -7965400327305809232L; + + final CompletableSubscriber actual; + final Completable[] sources; + + int index; + + final SerialSubscription sd; + + public ConcatInnerSubscriber(CompletableSubscriber actual, Completable[] sources) { + this.actual = actual; + this.sources = sources; + this.sd = new SerialSubscription(); + } + + @Override + public void onSubscribe(Subscription d) { + sd.set(d); + } + + @Override + public void onError(Throwable e) { + actual.onError(e); + } + + @Override + public void onCompleted() { + next(); + } + + void next() { + if (sd.isUnsubscribed()) { + return; + } + + if (getAndIncrement() != 0) { + return; + } + + Completable[] a = sources; + do { + if (sd.isUnsubscribed()) { + return; + } + + int idx = index++; + if (idx == a.length) { + actual.onCompleted(); + return; + } + + a[idx].subscribe(this); + } while (decrementAndGet() != 0); + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeConcatIterable.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcatIterable.java new file mode 100644 index 0000000000..fe6211153e --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeConcatIterable.java @@ -0,0 +1,135 @@ +/** + * 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.Iterator; +import java.util.concurrent.atomic.AtomicInteger; + +import rx.*; +import rx.Completable.*; +import rx.subscriptions.*; + +public final class CompletableOnSubscribeConcatIterable implements CompletableOnSubscribe { + final Iterable sources; + + public CompletableOnSubscribeConcatIterable(Iterable sources) { + this.sources = sources; + } + + @Override + public void call(CompletableSubscriber s) { + + Iterator it; + + try { + it = sources.iterator(); + } catch (Throwable e) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(e); + return; + } + + if (it == null) { + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(new NullPointerException("The iterator returned is null")); + return; + } + + ConcatInnerSubscriber inner = new ConcatInnerSubscriber(s, it); + s.onSubscribe(inner.sd); + inner.next(); + } + + static final class ConcatInnerSubscriber extends AtomicInteger implements CompletableSubscriber { + /** */ + private static final long serialVersionUID = -7965400327305809232L; + + final CompletableSubscriber actual; + final Iterator sources; + + int index; + + final SerialSubscription sd; + + public ConcatInnerSubscriber(CompletableSubscriber actual, Iterator sources) { + this.actual = actual; + this.sources = sources; + this.sd = new SerialSubscription(); + } + + @Override + public void onSubscribe(Subscription d) { + sd.set(d); + } + + @Override + public void onError(Throwable e) { + actual.onError(e); + } + + @Override + public void onCompleted() { + next(); + } + + void next() { + if (sd.isUnsubscribed()) { + return; + } + + if (getAndIncrement() != 0) { + return; + } + + Iterator a = sources; + do { + if (sd.isUnsubscribed()) { + return; + } + + boolean b; + try { + b = a.hasNext(); + } catch (Throwable ex) { + actual.onError(ex); + return; + } + + if (!b) { + actual.onCompleted(); + return; + } + + Completable c; + + try { + c = a.next(); + } catch (Throwable ex) { + actual.onError(ex); + return; + } + + if (c == null) { + actual.onError(new NullPointerException("The completable returned is null")); + return; + } + + c.subscribe(this); + } while (decrementAndGet() != 0); + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java new file mode 100644 index 0000000000..2b1a3ad2f0 --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java @@ -0,0 +1,215 @@ +/** + * 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.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.Completable.*; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.CompositeSubscription; + +public final class CompletableOnSubscribeMerge implements CompletableOnSubscribe { + final Observable source; + final int maxConcurrency; + final boolean delayErrors; + + public CompletableOnSubscribeMerge(Observable source, int maxConcurrency, boolean delayErrors) { + this.source = source; + this.maxConcurrency = maxConcurrency; + this.delayErrors = delayErrors; + } + + @Override + public void call(CompletableSubscriber s) { + CompletableMergeSubscriber parent = new CompletableMergeSubscriber(s, maxConcurrency, delayErrors); + s.onSubscribe(parent); + source.subscribe(parent); + } + + static final class CompletableMergeSubscriber + extends Subscriber { + final CompletableSubscriber actual; + final CompositeSubscription set; + final int maxConcurrency; + final boolean delayErrors; + + volatile boolean done; + + volatile Queue errors; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater ERRORS = + AtomicReferenceFieldUpdater.newUpdater(CompletableMergeSubscriber.class, Queue.class, "errors"); + + volatile int once; + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(CompletableMergeSubscriber.class, "once"); + + final AtomicInteger wip; + + public CompletableMergeSubscriber(CompletableSubscriber actual, int maxConcurrency, boolean delayErrors) { + this.actual = actual; + this.maxConcurrency = maxConcurrency; + this.delayErrors = delayErrors; + this.set = new CompositeSubscription(); + this.wip = new AtomicInteger(1); + if (maxConcurrency == Integer.MAX_VALUE) { + request(Long.MAX_VALUE); + } else { + request(maxConcurrency); + } + } + + Queue getOrCreateErrors() { + Queue q = errors; + + if (q != null) { + return q; + } + + q = new ConcurrentLinkedQueue(); + if (ERRORS.compareAndSet(this, null, q)) { + return q; + } + return errors; + } + + @Override + public void onNext(Completable t) { + if (done) { + return; + } + + wip.getAndIncrement(); + + t.subscribe(new CompletableSubscriber() { + Subscription d; + boolean innerDone; + @Override + public void onSubscribe(Subscription d) { + this.d = d; + set.add(d); + } + + @Override + public void onError(Throwable e) { + if (innerDone) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + return; + } + innerDone = true; + set.remove(d); + + getOrCreateErrors().offer(e); + + terminate(); + + if (delayErrors && !done) { + request(1); + } + } + + @Override + public void onCompleted() { + if (innerDone) { + return; + } + innerDone = true; + set.remove(d); + + terminate(); + + if (!done) { + request(1); + } + } + }); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(t); + return; + } + getOrCreateErrors().offer(t); + done = true; + terminate(); + } + + @Override + public void onCompleted() { + if (done) { + return; + } + done = true; + terminate(); + } + + void terminate() { + if (wip.decrementAndGet() == 0) { + Queue q = errors; + if (q == null || q.isEmpty()) { + actual.onCompleted(); + } else { + Throwable e = collectErrors(q); + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onError(e); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + } + } else + if (!delayErrors) { + Queue q = errors; + if (q != null && !q.isEmpty()) { + Throwable e = collectErrors(q); + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onError(e); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + } + } + } + } + + /** + * Collects the Throwables from the queue, adding subsequent Throwables as suppressed to + * the first Throwable and returns it. + * @param q the queue to drain + * @return the Throwable containing all other Throwables as suppressed + */ + public static Throwable collectErrors(Queue q) { + Throwable ex = null; + + Throwable t; + int count = 0; + while ((t = q.poll()) != null) { + if (count == 0) { + ex = t; + } else { + ex.addSuppressed(t); + } + + count++; + } + return ex; + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeArray.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeArray.java new file mode 100644 index 0000000000..85d3d59b3a --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeArray.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 java.util.concurrent.atomic.*; + +import rx.*; +import rx.Completable.*; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.CompositeSubscription; + +public final class CompletableOnSubscribeMergeArray implements CompletableOnSubscribe { + final Completable[] sources; + + public CompletableOnSubscribeMergeArray(Completable[] sources) { + this.sources = sources; + } + + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + final AtomicInteger wip = new AtomicInteger(sources.length + 1); + final AtomicBoolean once = new AtomicBoolean(); + + s.onSubscribe(set); + + for (Completable c : sources) { + if (set.isUnsubscribed()) { + return; + } + + if (c == null) { + set.unsubscribe(); + NullPointerException npe = new NullPointerException("A completable source is null"); + if (once.compareAndSet(false, true)) { + s.onError(npe); + return; + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(npe); + } + } + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.unsubscribe(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + } + + @Override + public void onCompleted() { + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onCompleted(); + } + } + } + + }); + } + + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onCompleted(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorArray.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorArray.java new file mode 100644 index 0000000000..2a89afbaa2 --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorArray.java @@ -0,0 +1,93 @@ +/** + * 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.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import rx.*; +import rx.Completable.*; +import rx.subscriptions.CompositeSubscription; + +public final class CompletableOnSubscribeMergeDelayErrorArray implements CompletableOnSubscribe { + final Completable[] sources; + + public CompletableOnSubscribeMergeDelayErrorArray(Completable[] sources) { + this.sources = sources; + } + + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + final AtomicInteger wip = new AtomicInteger(sources.length + 1); + + final Queue q = new ConcurrentLinkedQueue(); + + s.onSubscribe(set); + + for (Completable c : sources) { + if (set.isUnsubscribed()) { + return; + } + + if (c == null) { + q.offer(new NullPointerException("A completable source is null")); + wip.decrementAndGet(); + continue; + } + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + q.offer(e); + tryTerminate(); + } + + @Override + public void onCompleted() { + tryTerminate(); + } + + void tryTerminate() { + if (wip.decrementAndGet() == 0) { + if (q.isEmpty()) { + s.onCompleted(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(q)); + } + } + } + + }); + } + + if (wip.decrementAndGet() == 0) { + if (q.isEmpty()) { + s.onCompleted(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(q)); + } + } + + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorIterable.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorIterable.java new file mode 100644 index 0000000000..be783e6d6d --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeDelayErrorIterable.java @@ -0,0 +1,157 @@ +/** + * 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.*; +import java.util.concurrent.atomic.AtomicInteger; + +import rx.*; +import rx.Completable.*; +import rx.internal.util.unsafe.MpscLinkedQueue; +import rx.subscriptions.CompositeSubscription; + +public final class CompletableOnSubscribeMergeDelayErrorIterable implements CompletableOnSubscribe { + final Iterable sources; + + public CompletableOnSubscribeMergeDelayErrorIterable(Iterable sources) { + this.sources = sources; + } + + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + final AtomicInteger wip = new AtomicInteger(1); + + final Queue queue = new MpscLinkedQueue(); + + s.onSubscribe(set); + + Iterator iterator; + + try { + iterator = sources.iterator(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (iterator == null) { + s.onError(new NullPointerException("The source iterator returned is null")); + return; + } + + for (;;) { + if (set.isUnsubscribed()) { + return; + } + + boolean b; + try { + b = iterator.hasNext(); + } catch (Throwable e) { + queue.offer(e); + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onCompleted(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + return; + } + + if (!b) { + break; + } + + if (set.isUnsubscribed()) { + return; + } + + Completable c; + + try { + c = iterator.next(); + } catch (Throwable e) { + queue.offer(e); + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onCompleted(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + return; + } + + if (set.isUnsubscribed()) { + return; + } + + if (c == null) { + NullPointerException e = new NullPointerException("A completable source is null"); + queue.offer(e); + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onCompleted(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + return; + } + + wip.getAndIncrement(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + queue.offer(e); + tryTerminate(); + } + + @Override + public void onCompleted() { + tryTerminate(); + } + + void tryTerminate() { + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onCompleted(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + } + }); + } + + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onCompleted(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeIterable.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeIterable.java new file mode 100644 index 0000000000..7ad953e4de --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeMergeIterable.java @@ -0,0 +1,147 @@ +/** + * 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.Iterator; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.Completable.*; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.CompositeSubscription; + +public final class CompletableOnSubscribeMergeIterable implements CompletableOnSubscribe { + final Iterable sources; + + public CompletableOnSubscribeMergeIterable(Iterable sources) { + this.sources = sources; + } + + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + final AtomicInteger wip = new AtomicInteger(1); + final AtomicBoolean once = new AtomicBoolean(); + + s.onSubscribe(set); + + Iterator iterator; + + try { + iterator = sources.iterator(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (iterator == null) { + s.onError(new NullPointerException("The source iterator returned is null")); + return; + } + + for (;;) { + if (set.isUnsubscribed()) { + return; + } + + boolean b; + try { + b = iterator.hasNext(); + } catch (Throwable e) { + set.unsubscribe(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + return; + } + + if (!b) { + break; + } + + if (set.isUnsubscribed()) { + return; + } + + Completable c; + + try { + c = iterator.next(); + } catch (Throwable e) { + set.unsubscribe(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + return; + } + + if (set.isUnsubscribed()) { + return; + } + + if (c == null) { + set.unsubscribe(); + NullPointerException npe = new NullPointerException("A completable source is null"); + if (once.compareAndSet(false, true)) { + s.onError(npe); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(npe); + } + return; + } + + wip.getAndIncrement(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.unsubscribe(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + } + + @Override + public void onCompleted() { + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onCompleted(); + } + } + } + + }); + } + + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onCompleted(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeTimeout.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeTimeout.java new file mode 100644 index 0000000000..2a9c8e31e2 --- /dev/null +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeTimeout.java @@ -0,0 +1,115 @@ +/** + * 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.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import rx.*; +import rx.Completable.*; +import rx.functions.Action0; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.CompositeSubscription; + +public final class CompletableOnSubscribeTimeout implements CompletableOnSubscribe { + + final Completable source; + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final Completable other; + + public CompletableOnSubscribeTimeout(Completable source, long timeout, + TimeUnit unit, Scheduler scheduler, Completable other) { + this.source = source; + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.other = other; + } + + @Override + public void call(final CompletableSubscriber s) { + final CompositeSubscription set = new CompositeSubscription(); + s.onSubscribe(set); + + final AtomicBoolean once = new AtomicBoolean(); + + Scheduler.Worker w = scheduler.createWorker(); + + set.add(w); + w.schedule(new Action0() { + @Override + public void call() { + if (once.compareAndSet(false, true)) { + set.clear(); + if (other == null) { + s.onError(new TimeoutException()); + } else { + other.subscribe(new CompletableSubscriber() { + + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.unsubscribe(); + s.onError(e); + } + + @Override + public void onCompleted() { + set.unsubscribe(); + s.onCompleted(); + } + + }); + } + } + } + }, timeout, unit); + + source.subscribe(new CompletableSubscriber() { + + @Override + public void onSubscribe(Subscription d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onError(e); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + } + + @Override + public void onCompleted() { + if (once.compareAndSet(false, true)) { + set.unsubscribe(); + s.onCompleted(); + } + } + + }); + } +} \ No newline at end of file diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java new file mode 100644 index 0000000000..ec73ad0141 --- /dev/null +++ b/src/test/java/rx/CompletableTest.java @@ -0,0 +1,3413 @@ +/** + * 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; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import rx.Completable.*; +import rx.exceptions.*; +import rx.functions.*; +import rx.observers.TestSubscriber; +import rx.plugins.RxJavaPlugins; +import rx.schedulers.*; +import rx.subjects.PublishSubject; +import rx.subscriptions.*; + +/** + * Test Completable methods and operators. + */ +public class CompletableTest { + /** + * Iterable that returns an Iterator that throws in its hasNext method. + */ + static final class IterableIteratorNextThrows implements Iterable { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Completable next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + } + + /** + * Iterable that returns an Iterator that throws in its next method. + */ + static final class IterableIteratorHasNextThrows implements Iterable { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + throw new TestException(); + } + + @Override + public Completable next() { + return null; + } + + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + } + + /** + * A class containing a completable instance and counts the number of subscribers. + */ + static final class NormalCompletable extends AtomicInteger { + /** */ + private static final long serialVersionUID = 7192337844700923752L; + + public final Completable completable = Completable.create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + getAndIncrement(); + s.onSubscribe(Subscriptions.unsubscribed()); + s.onCompleted(); + } + }); + + /** + * Asserts the given number of subscriptions happened. + * @param n the expected number of subscriptions + */ + public void assertSubscriptions(int n) { + Assert.assertEquals(n, get()); + } + } + + /** + * A class containing a completable instance that emits a TestException and counts + * the number of subscribers. + */ + static final class ErrorCompletable extends AtomicInteger { + /** */ + private static final long serialVersionUID = 7192337844700923752L; + + public final Completable completable = Completable.create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + getAndIncrement(); + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(new TestException()); + } + }); + + /** + * Asserts the given number of subscriptions happened. + * @param n the expected number of subscriptions + */ + public void assertSubscriptions(int n) { + Assert.assertEquals(n, get()); + } + } + + /** A normal Completable object. */ + final NormalCompletable normal = new NormalCompletable(); + + /** An error Completable object. */ + final ErrorCompletable error = new ErrorCompletable(); + + @Test(timeout = 1000) + public void complete() { + Completable c = Completable.complete(); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void concatNull() { + Completable.concat((Completable[])null); + } + + @Test(timeout = 1000) + public void concatEmpty() { + Completable c = Completable.concat(); + + c.await(); + } + + @Test(timeout = 1000) + public void concatSingleSource() { + Completable c = Completable.concat(normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatSingleSourceThrows() { + Completable c = Completable.concat(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void concatMultipleSources() { + Completable c = Completable.concat(normal.completable, normal.completable, normal.completable); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatMultipleOneThrows() { + Completable c = Completable.concat(normal.completable, error.completable, normal.completable); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void concatMultipleOneIsNull() { + Completable c = Completable.concat(normal.completable, null); + + c.await(); + } + + @Test(timeout = 1000) + public void concatIterableEmpty() { + Completable c = Completable.concat(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableNull() { + Completable.concat((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void concatIterableIteratorNull() { + Completable c = Completable.concat(new Iterable() { + @Override + public Iterator iterator() { + return null; + } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void concatIterableWithNull() { + Completable c = Completable.concat(Arrays.asList(normal.completable, (Completable)null)); + + c.await(); + } + + @Test(timeout = 1000) + public void concatIterableSingle() { + Completable c = Completable.concat(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void concatIterableMany() { + Completable c = Completable.concat(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatIterableOneThrows() { + Completable c = Completable.concat(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatIterableManyOneThrows() { + Completable c = Completable.concat(Arrays.asList(normal.completable, error.completable)); + + c.await(); + } + + @Test(expected = TestException.class) + public void concatIterableIterableThrows() { + Completable c = Completable.concat(new Iterable() { + @Override + public Iterator iterator() { + throw new TestException(); + } + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void concatIterableIteratorHasNextThrows() { + Completable c = Completable.concat(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void concatIterableIteratorNextThrows() { + Completable c = Completable.concat(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservableEmpty() { + Completable c = Completable.concat(Observable.empty()); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatObservableError() { + Completable c = Completable.concat(Observable.error(new TestException())); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservableSingle() { + Completable c = Completable.concat(Observable.just(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatObservableSingleThrows() { + Completable c = Completable.concat(Observable.just(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservableMany() { + Completable c = Completable.concat(Observable.just(normal.completable).repeat(3)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatObservableManyOneThrows() { + Completable c = Completable.concat(Observable.just(normal.completable, error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservablePrefetch() { + final List requested = new ArrayList(); + Observable cs = Observable + .just(normal.completable) + .repeat(10) + .doOnRequest(new Action1() { + @Override + public void call(Long v) { + requested.add(v); + } + }); + + Completable c = Completable.concat(cs, 5); + + c.await(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test(expected = NullPointerException.class) + public void createNull() { + Completable.create(null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void createOnSubscribeThrowsNPE() { + Completable c = Completable.create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { throw new NullPointerException(); } + }); + + c.await(); + } + + @Test(timeout = 1000) + public void createOnSubscribeThrowsRuntimeException() { + try { + Completable c = Completable.create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + throw new TestException(); + } + }); + + c.await(); + + Assert.fail("Did not throw exception"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof TestException)) { + ex.printStackTrace(); + Assert.fail("Did not wrap the TestException but it returned: " + ex); + } + } + } + + @Test(timeout = 1000) + public void defer() { + Completable c = Completable.defer(new Func0() { + @Override + public Completable call() { + return normal.completable; + } + }); + + normal.assertSubscriptions(0); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(expected = NullPointerException.class) + public void deferNull() { + Completable.defer(null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void deferReturnsNull() { + Completable c = Completable.defer(new Func0() { + @Override + public Completable call() { + return null; + } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void deferFunctionThrows() { + Completable c = Completable.defer(new Func0() { + @Override + public Completable call() { throw new TestException(); } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void deferErrorSource() { + Completable c = Completable.defer(new Func0() { + @Override + public Completable call() { + return error.completable; + } + }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void errorNull() { + Completable.error((Throwable)null); + } + + @Test(timeout = 1000, expected = TestException.class) + public void errorNormal() { + Completable c = Completable.error(new TestException()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromCallableNull() { + Completable.fromCallable(null); + } + + @Test(timeout = 1000) + public void fromCallableNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + return calls.getAndIncrement(); + } + }); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromCallableThrows() { + Completable c = Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { throw new TestException(); } + }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromObservableNull() { + Completable.fromObservable(null); + } + + @Test(timeout = 1000) + public void fromObservableEmpty() { + Completable c = Completable.fromObservable(Observable.empty()); + + c.await(); + } + + @Test(timeout = 5000) + public void fromObservableSome() { + for (int n = 1; n < 10000; n *= 10) { + Completable c = Completable.fromObservable(Observable.range(1, n)); + + c.await(); + } + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromObservableError() { + Completable c = Completable.fromObservable(Observable.error(new TestException())); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromFutureNull() { + Completable.fromFuture(null); + } + + @Test(timeout = 1000) + public void fromFutureNormal() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + + try { + Completable c = Completable.fromFuture(exec.submit(new Runnable() { + @Override + public void run() { + // no action + } + })); + + c.await(); + } finally { + exec.shutdown(); + } + } + + @Test(timeout = 1000) + public void fromFutureThrows() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + + Completable c = Completable.fromFuture(exec.submit(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + })); + + try { + c.await(); + Assert.fail("Failed to throw Exception"); + } catch (RuntimeException ex) { + if (!((ex.getCause() instanceof ExecutionException) && (ex.getCause().getCause() instanceof TestException))) { + ex.printStackTrace(); + Assert.fail("Wrong exception received"); + } + } finally { + exec.shutdown(); + } + } + + @Test(expected = NullPointerException.class) + public void fromActionNull() { + Completable.fromAction(null); + } + + @Test(timeout = 1000) + public void fromActionNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromAction(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromActionThrows() { + Completable c = Completable.fromAction(new Action0() { + @Override + public void call() { throw new TestException(); } + }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromSingleNull() { + Completable.fromSingle(null); + } + + @Test(timeout = 1000) + public void fromSingleNormal() { + Completable c = Completable.fromSingle(Single.just(1)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromSingleThrows() { + Completable c = Completable.fromSingle(Single.error(new TestException())); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeNull() { + Completable.merge((Completable[])null); + } + + @Test(timeout = 1000) + public void mergeEmpty() { + Completable c = Completable.merge(); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeSingleSource() { + Completable c = Completable.merge(normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeSingleSourceThrows() { + Completable c = Completable.merge(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeMultipleSources() { + Completable c = Completable.merge(normal.completable, normal.completable, normal.completable); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeMultipleOneThrows() { + Completable c = Completable.merge(normal.completable, error.completable, normal.completable); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeMultipleOneIsNull() { + Completable c = Completable.merge(normal.completable, null); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeIterableEmpty() { + Completable c = Completable.merge(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeIterableNull() { + Completable.merge((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeIterableIteratorNull() { + Completable c = Completable.merge(new Iterable() { + @Override + public Iterator iterator() { + return null; + } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeIterableWithNull() { + Completable c = Completable.merge(Arrays.asList(normal.completable, (Completable)null)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeIterableSingle() { + Completable c = Completable.merge(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void mergeIterableMany() { + Completable c = Completable.merge(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeIterableOneThrows() { + Completable c = Completable.merge(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeIterableManyOneThrows() { + Completable c = Completable.merge(Arrays.asList(normal.completable, error.completable)); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeIterableIterableThrows() { + Completable c = Completable.merge(new Iterable() { + @Override + public Iterator iterator() { + throw new TestException(); + } + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeIterableIteratorHasNextThrows() { + Completable c = Completable.merge(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeIterableIteratorNextThrows() { + Completable c = Completable.merge(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableEmpty() { + Completable c = Completable.merge(Observable.empty()); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeObservableError() { + Completable c = Completable.merge(Observable.error(new TestException())); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableSingle() { + Completable c = Completable.merge(Observable.just(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeObservableSingleThrows() { + Completable c = Completable.merge(Observable.just(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableMany() { + Completable c = Completable.merge(Observable.just(normal.completable).repeat(3)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeObservableManyOneThrows() { + Completable c = Completable.merge(Observable.just(normal.completable, error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableMaxConcurrent() { + final List requested = new ArrayList(); + Observable cs = Observable + .just(normal.completable) + .repeat(10) + .doOnRequest(new Action1() { + @Override + public void call(Long v) { + requested.add(v); + } + }); + + Completable c = Completable.merge(cs, 5); + + c.await(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorNull() { + Completable.mergeDelayError((Completable[])null); + } + + @Test(timeout = 1000) + public void mergeDelayErrorEmpty() { + Completable c = Completable.mergeDelayError(); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorSingleSource() { + Completable c = Completable.mergeDelayError(normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorSingleSourceThrows() { + Completable c = Completable.mergeDelayError(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorMultipleSources() { + Completable c = Completable.mergeDelayError(normal.completable, normal.completable, normal.completable); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000) + public void mergeDelayErrorMultipleOneThrows() { + Completable c = Completable.mergeDelayError(normal.completable, error.completable, normal.completable); + + try { + c.await(); + } catch (TestException ex) { + normal.assertSubscriptions(2); + } + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeDelayErrorMultipleOneIsNull() { + Completable c = Completable.mergeDelayError(normal.completable, null); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableEmpty() { + Completable c = Completable.mergeDelayError(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorIterableNull() { + Completable.mergeDelayError((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeDelayErrorIterableIteratorNull() { + Completable c = Completable.mergeDelayError(new Iterable() { + @Override + public Iterator iterator() { + return null; + } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeDelayErrorIterableWithNull() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, (Completable)null)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableSingle() { + Completable c = Completable.mergeDelayError(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableMany() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorIterableOneThrows() { + Completable c = Completable.mergeDelayError(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableManyOneThrows() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, error.completable, normal.completable)); + + try { + c.await(); + } catch (TestException ex) { + normal.assertSubscriptions(2); + } + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIterableThrows() { + Completable c = Completable.mergeDelayError(new Iterable() { + @Override + public Iterator iterator() { + throw new TestException(); + } + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIteratorHasNextThrows() { + Completable c = Completable.mergeDelayError(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIteratorNextThrows() { + Completable c = Completable.mergeDelayError(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableEmpty() { + Completable c = Completable.mergeDelayError(Observable.empty()); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorObservableError() { + Completable c = Completable.mergeDelayError(Observable.error(new TestException())); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableSingle() { + Completable c = Completable.mergeDelayError(Observable.just(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorObservableSingleThrows() { + Completable c = Completable.mergeDelayError(Observable.just(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableMany() { + Completable c = Completable.mergeDelayError(Observable.just(normal.completable).repeat(3)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorObservableManyOneThrows() { + Completable c = Completable.mergeDelayError(Observable.just(normal.completable, error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableMaxConcurrent() { + final List requested = new ArrayList(); + Observable cs = Observable + .just(normal.completable) + .repeat(10) + .doOnRequest(new Action1() { + @Override + public void call(Long v) { + requested.add(v); + } + }); + + Completable c = Completable.mergeDelayError(cs, 5); + + c.await(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test(timeout = 1000) + public void never() { + final AtomicBoolean onSubscribeCalled = new AtomicBoolean(); + final AtomicInteger calls = new AtomicInteger(); + Completable.never().subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + onSubscribeCalled.set(true); + } + + @Override + public void onError(Throwable e) { + calls.getAndIncrement(); + } + + @Override + public void onCompleted() { + calls.getAndIncrement(); + } + }); + + Assert.assertTrue("onSubscribe not called", onSubscribeCalled.get()); + Assert.assertEquals("There were calls to onXXX methods", 0, calls.get()); + } + + @Test(timeout = 1500) + public void timer() { + Completable c = Completable.timer(500, TimeUnit.MILLISECONDS); + + c.await(); + } + + @Test(timeout = 1500) + public void timerNewThread() { + Completable c = Completable.timer(500, TimeUnit.MILLISECONDS, Schedulers.newThread()); + + c.await(); + } + + @Test(timeout = 1000) + public void timerTestScheduler() { + TestScheduler scheduler = Schedulers.test(); + + Completable c = Completable.timer(250, TimeUnit.MILLISECONDS, scheduler); + + final AtomicInteger calls = new AtomicInteger(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onCompleted() { + calls.getAndIncrement(); + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } + }); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + Assert.assertEquals(0, calls.get()); + + scheduler.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 2000) + public void timerCancel() throws InterruptedException { + Completable c = Completable.timer(250, TimeUnit.MILLISECONDS); + + final MultipleAssignmentSubscription mad = new MultipleAssignmentSubscription(); + final AtomicInteger calls = new AtomicInteger(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + mad.set(d); + } + + @Override + public void onError(Throwable e) { + calls.getAndIncrement(); + } + + @Override + public void onCompleted() { + calls.getAndIncrement(); + } + }); + + Thread.sleep(100); + + mad.unsubscribe(); + + Thread.sleep(200); + + Assert.assertEquals(0, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void timerUnitNull() { + Completable.timer(1, null); + } + + @Test(expected = NullPointerException.class) + public void timerSchedulerNull() { + Completable.timer(1, TimeUnit.SECONDS, null); + } + + @Test(timeout = 1000) + public void usingNormalEager() { + final AtomicInteger unsubscribe = new AtomicInteger(); + + Completable c = Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, new Func1() { + @Override + public Completable call(Object v) { + return normal.completable; + } + }, new Action1() { + @Override + public void call(Integer d) { + unsubscribe.set(d); + } + }); + + final AtomicBoolean unsubscribedFirst = new AtomicBoolean(); + final AtomicReference error = new AtomicReference(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + error.lazySet(e); + } + + @Override + public void onCompleted() { + unsubscribedFirst.set(unsubscribe.get() != 0); + } + }); + + Assert.assertEquals(1, unsubscribe.get()); + Assert.assertTrue("Not unsubscribed first", unsubscribedFirst.get()); + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void usingNormalLazy() { + final AtomicInteger unsubscribe = new AtomicInteger(); + + Completable c = Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, new Func1() { + @Override + public Completable call(Integer v) { + return normal.completable; + } + }, new Action1() { + @Override + public void call(Integer d) { + unsubscribe.set(d); + } + }, false); + + final AtomicBoolean unsubscribedFirst = new AtomicBoolean(); + final AtomicReference error = new AtomicReference(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + error.lazySet(e); + } + + @Override + public void onCompleted() { + unsubscribedFirst.set(unsubscribe.get() != 0); + } + }); + + Assert.assertEquals(1, unsubscribe.get()); + Assert.assertFalse("Disposed first", unsubscribedFirst.get()); + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void usingErrorEager() { + final AtomicInteger unsubscribe = new AtomicInteger(); + + Completable c = Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, new Func1() { + @Override + public Completable call(Integer v) { + return error.completable; + } + }, new Action1() { + @Override + public void call(Integer d) { + unsubscribe.set(d); + } + }); + + final AtomicBoolean unsubscribedFirst = new AtomicBoolean(); + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + unsubscribedFirst.set(unsubscribe.get() != 0); + } + + @Override + public void onCompleted() { + complete.set(true); + } + }); + + Assert.assertEquals(1, unsubscribe.get()); + Assert.assertTrue("Not unsubscribed first", unsubscribedFirst.get()); + Assert.assertFalse(complete.get()); + } + + @Test(timeout = 1000) + public void usingErrorLazy() { + final AtomicInteger unsubscribe = new AtomicInteger(); + + Completable c = Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, new Func1() { + @Override + public Completable call(Integer v) { + return error.completable; + } + }, new Action1() { + @Override + public void call(Integer d) { + unsubscribe.set(d); + } + }, false); + + final AtomicBoolean unsubscribedFirst = new AtomicBoolean(); + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + unsubscribedFirst.set(unsubscribe.get() != 0); + } + + @Override + public void onCompleted() { + complete.set(true); + } + }); + + Assert.assertEquals(1, unsubscribe.get()); + Assert.assertFalse("Disposed first", unsubscribedFirst.get()); + Assert.assertFalse(complete.get()); + } + + @Test(expected = NullPointerException.class) + public void usingResourceSupplierNull() { + Completable.using(null, new Func1() { + @Override + public Completable call(Object v) { + return normal.completable; + } + }, new Action1() { + @Override + public void call(Object v) { } + }); + } + + @Test(expected = NullPointerException.class) + public void usingMapperNull() { + Completable.using(new Func0() { + @Override + public Object call() { + return 1; + } + }, null, new Action1() { + @Override + public void call(Object v) { } + }); + } + + @Test(expected = NullPointerException.class) + public void usingMapperReturnsNull() { + Completable c = Completable.using(new Func0() { + @Override + public Object call() { + return 1; + } + }, new Func1() { + @Override + public Completable call(Object v) { + return null; + } + }, new Action1() { + @Override + public void call(Object v) { } + }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void usingDisposeNull() { + Completable.using(new Func0() { + @Override + public Object call() { + return 1; + } + }, new Func1() { + @Override + public Completable call(Object v) { + return normal.completable; + } + }, null); + } + + @Test(expected = TestException.class) + public void usingResourceThrows() { + Completable c = Completable.using(new Func0() { + @Override + public Object call() { throw new TestException(); } + }, + new Func1() { + @Override + public Completable call(Object v) { + return normal.completable; + } + }, new Action1() { + @Override + public void call(Object v) { } + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void usingMapperThrows() { + Completable c = Completable.using(new Func0() { + @Override + public Object call() { + return 1; + } + }, + new Func1() { + @Override + public Completable call(Object v) { throw new TestException(); } + }, new Action1() { + @Override + public void call(Object v) { } + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void usingDisposerThrows() { + Completable c = Completable.using(new Func0() { + @Override + public Object call() { + return 1; + } + }, + new Func1() { + @Override + public Completable call(Object v) { + return normal.completable; + } + }, new Action1() { + @Override + public void call(Object v) { throw new TestException(); } + }); + + c.await(); + } + + @Test(timeout = 1000) + public void composeNormal() { + Completable c = error.completable.compose(new CompletableTransformer() { + @Override + public Completable call(Completable n) { + return n.onErrorComplete(); + } + }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void composeNull() { + error.completable.compose(null); + } + + @Test(timeout = 1000) + public void concatWithNormal() { + Completable c = normal.completable.concatWith(normal.completable); + + c.await(); + + normal.assertSubscriptions(2); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatWithError() { + Completable c = normal.completable.concatWith(error.completable); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void concatWithNull() { + normal.completable.concatWith(null); + } + + @Test(expected = NullPointerException.class) + public void delayUnitNull() { + normal.completable.delay(1, null); + } + + @Test(expected = NullPointerException.class) + public void delaySchedulerNull() { + normal.completable.delay(1, TimeUnit.SECONDS, null); + } + + @Test(timeout = 1000) + public void delayNormal() throws InterruptedException { + Completable c = normal.completable.delay(250, TimeUnit.MILLISECONDS); + + final AtomicBoolean done = new AtomicBoolean(); + final AtomicReference error = new AtomicReference(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onCompleted() { + done.set(true); + } + }); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + + Thread.sleep(200); + + Assert.assertTrue("Not done", done.get()); + + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void delayErrorImmediately() throws InterruptedException { + Completable c = error.completable.delay(250, TimeUnit.MILLISECONDS); + + final AtomicBoolean done = new AtomicBoolean(); + final AtomicReference error = new AtomicReference(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onCompleted() { + done.set(true); + } + }); + + Assert.assertTrue(error.get().toString(), error.get() instanceof TestException); + Assert.assertFalse("Already done", done.get()); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + + Thread.sleep(200); + } + + @Test(timeout = 1000) + public void delayErrorToo() throws InterruptedException { + Completable c = error.completable.delay(250, TimeUnit.MILLISECONDS, Schedulers.computation(), true); + + final AtomicBoolean done = new AtomicBoolean(); + final AtomicReference error = new AtomicReference(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onCompleted() { + done.set(true); + } + }); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + Assert.assertNull(error.get()); + + Thread.sleep(200); + + Assert.assertFalse("Already done", done.get()); + Assert.assertTrue(error.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void doOnCompleteNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnComplete(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void doOnCompleteError() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnComplete(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + try { + c.await(); + Assert.fail("Failed to throw TestException"); + } catch (TestException ex) { + // expected + } + + Assert.assertEquals(0, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void doOnCompleteNull() { + normal.completable.doOnComplete(null); + } + + @Test(timeout = 1000, expected = TestException.class) + public void doOnCompleteThrows() { + Completable c = normal.completable.doOnComplete(new Action0() { + @Override + public void call() { throw new TestException(); } + }); + + c.await(); + } + + @Test(timeout = 1000) + public void doOnDisposeNormalDoesntCall() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnUnsubscribe(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + c.await(); + + Assert.assertEquals(0, calls.get()); + } + + @Test(timeout = 1000) + public void doOnDisposeErrorDoesntCall() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnUnsubscribe(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + try { + c.await(); + Assert.fail("No exception thrown"); + } catch (TestException ex) { + // expected + } + Assert.assertEquals(0, calls.get()); + } + + @Test(timeout = 1000) + public void doOnDisposeChildCancels() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnUnsubscribe(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + d.unsubscribe(); + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onCompleted() { + // ignored + } + }); + + Assert.assertEquals(1, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void doOnDisposeNull() { + normal.completable.doOnUnsubscribe(null); + } + + @Test(timeout = 1000) + public void doOnDisposeThrows() { + Completable c = normal.completable.doOnUnsubscribe(new Action0() { + @Override + public void call() { throw new TestException(); } + }); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + d.unsubscribe(); + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onCompleted() { + // ignored + } + }); + } + + @Test(timeout = 1000) + public void doOnErrorNoError() { + final AtomicReference error = new AtomicReference(); + + Completable c = normal.completable.doOnError(new Action1() { + @Override + public void call(Throwable e) { + error.set(e); + } + }); + + c.await(); + + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void doOnErrorHasError() { + final AtomicReference err = new AtomicReference(); + + Completable c = error.completable.doOnError(new Action1() { + @Override + public void call(Throwable e) { + err.set(e); + } + }); + + try { + c.await(); + Assert.fail("Did not throw exception"); + } catch (Throwable e) { + // expected + } + + Assert.assertTrue(err.get() instanceof TestException); + } + + @Test(expected = NullPointerException.class) + public void doOnErrorNull() { + normal.completable.doOnError(null); + } + + @Test(timeout = 1000) + public void doOnErrorThrows() { + Completable c = error.completable.doOnError(new Action1() { + @Override + public void call(Throwable e) { throw new IllegalStateException(); } + }); + + try { + c.await(); + } catch (IllegalStateException ex) { + Throwable[] a = ex.getSuppressed(); + Assert.assertEquals(1, a.length); + Assert.assertTrue(a[0] instanceof TestException); + } + } + + @Test(timeout = 1000) + public void doOnSubscribeNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnSubscribe(new Action1() { + @Override + public void call(Subscription s) { + calls.getAndIncrement(); + } + }); + + for (int i = 0; i < 10; i++) { + c.await(); + } + + Assert.assertEquals(10, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void doOnSubscribeNull() { + normal.completable.doOnSubscribe(null); + } + + @Test(expected = TestException.class) + public void doOnSubscribeThrows() { + Completable c = normal.completable.doOnSubscribe(new Action1() { + @Override + public void call(Subscription d) { throw new TestException(); } + }); + + c.await(); + } + + @Test(timeout = 1000) + public void doOnTerminateNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnTerminate(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void doOnTerminateError() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnTerminate(new Action0() { + @Override + public void call() { + calls.getAndIncrement(); + } + }); + + try { + c.await(); + Assert.fail("Did dot throw exception"); + } catch (TestException ex) { + // expected + } + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void finallyDoNormal() { + final AtomicBoolean doneAfter = new AtomicBoolean(); + final AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable.finallyDo(new Action0() { + @Override + public void call() { + doneAfter.set(complete.get()); + } + }); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onCompleted() { + complete.set(true); + } + }); + + c.await(); + + Assert.assertTrue("Not completed", complete.get()); + Assert.assertTrue("Finally called before onComplete", doneAfter.get()); + } + + @Test(timeout = 1000) + public void finallyDoWithError() { + final AtomicBoolean doneAfter = new AtomicBoolean(); + + Completable c = error.completable.finallyDo(new Action0() { + @Override + public void call() { + doneAfter.set(true); + } + }); + + try { + c.await(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + // expected + } + + Assert.assertFalse("FinallyDo called", doneAfter.get()); + } + + @Test(expected = NullPointerException.class) + public void finallyDoNull() { + normal.completable.finallyDo(null); + } + + @Test(timeout = 1000) + public void getNormal() { + Assert.assertNull(normal.completable.get()); + } + + @Test(timeout = 1000) + public void getError() { + Assert.assertTrue(error.completable.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void getTimeout() { + try { + Completable.never().get(100, TimeUnit.MILLISECONDS); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof TimeoutException)) { + Assert.fail("Wrong exception cause: " + ex.getCause()); + } + } + } + + @Test(expected = NullPointerException.class) + public void getNullUnit() { + normal.completable.get(1, null); + } + + @Test(expected = NullPointerException.class) + public void liftNull() { + normal.completable.lift(null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void liftReturnsNull() { + Completable c = normal.completable.lift(new CompletableOperator() { + @Override + public CompletableSubscriber call(CompletableSubscriber v) { + return null; + } + }); + + c.await(); + } + + final static class CompletableOperatorSwap implements CompletableOperator { + @Override + public CompletableSubscriber call(final CompletableSubscriber v) { + return new CompletableSubscriber() { + + @Override + public void onCompleted() { + v.onError(new TestException()); + } + + @Override + public void onError(Throwable e) { + v.onCompleted(); + } + + @Override + public void onSubscribe(Subscription d) { + v.onSubscribe(d); + } + + }; + } + } + @Test(timeout = 1000, expected = TestException.class) + public void liftOnCompleteError() { + Completable c = normal.completable.lift(new CompletableOperatorSwap()); + + c.await(); + } + + @Test(timeout = 1000) + public void liftOnErrorComplete() { + Completable c = error.completable.lift(new CompletableOperatorSwap()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeWithNull() { + normal.completable.mergeWith(null); + } + + @Test(timeout = 1000) + public void mergeWithNormal() { + Completable c = normal.completable.mergeWith(normal.completable); + + c.await(); + + normal.assertSubscriptions(2); + } + + @Test(expected = NullPointerException.class) + public void observeOnNull() { + normal.completable.observeOn(null); + } + + @Test(timeout = 1000) + public void observeOnNormal() throws InterruptedException { + final AtomicReference name = new AtomicReference(); + final AtomicReference err = new AtomicReference(); + final CountDownLatch cdl = new CountDownLatch(1); + + Completable c = normal.completable.observeOn(Schedulers.computation()); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onCompleted() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err.set(e); + cdl.countDown(); + } + }); + + cdl.await(); + + Assert.assertNull(err.get()); + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void observeOnError() throws InterruptedException { + final AtomicReference name = new AtomicReference(); + final AtomicReference err = new AtomicReference(); + final CountDownLatch cdl = new CountDownLatch(1); + + Completable c = error.completable.observeOn(Schedulers.computation()); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Subscription d) { + + } + + @Override + public void onCompleted() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + name.set(Thread.currentThread().getName()); + err.set(e); + cdl.countDown(); + } + }); + + cdl.await(); + + Assert.assertTrue(err.get() instanceof TestException); + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void onErrorComplete() { + Completable c = error.completable.onErrorComplete(); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void onErrorCompleteFalse() { + Completable c = error.completable.onErrorComplete(new Func1() { + @Override + public Boolean call(Throwable e) { + return e instanceof IllegalStateException; + } + }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void onErrorCompleteNull() { + error.completable.onErrorComplete(null); + } + + @Test(expected = NullPointerException.class) + public void onErrorResumeNextNull() { + error.completable.onErrorResumeNext(null); + } + + @Test(timeout = 1000) + public void onErrorResumeNextFunctionReturnsNull() { + Completable c = error.completable.onErrorResumeNext(new Func1() { + @Override + public Completable call(Throwable e) { + return null; + } + }); + + try { + c.await(); + Assert.fail("Did not throw an exception"); + } catch (NullPointerException ex) { + Throwable[] a = ex.getSuppressed(); + + Assert.assertEquals(1, a.length); + Assert.assertTrue(a[0] instanceof TestException); + } + } + + @Test(timeout = 1000) + public void onErrorResumeNextFunctionThrows() { + Completable c = error.completable.onErrorResumeNext(new Func1() { + @Override + public Completable call(Throwable e) { throw new TestException(); } + }); + + try { + c.await(); + Assert.fail("Did not throw an exception"); + } catch (TestException ex) { + Throwable[] a = ex.getSuppressed(); + + Assert.assertEquals(1, a.length); + Assert.assertTrue(a[0] instanceof TestException); + } + } + + @Test(timeout = 1000) + public void onErrorResumeNextNormal() { + Completable c = error.completable.onErrorResumeNext(new Func1() { + @Override + public Completable call(Throwable v) { + return normal.completable; + } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void onErrorResumeNextError() { + Completable c = error.completable.onErrorResumeNext(new Func1() { + @Override + public Completable call(Throwable v) { + return error.completable; + } + }); + + c.await(); + } + + @Test(timeout = 2000) + public void repeatNormal() { + final AtomicReference err = new AtomicReference(); + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + Thread.sleep(100); + return null; + } + }).repeat(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(final Subscription d) { + final Scheduler.Worker w = Schedulers.io().createWorker(); + w.schedule(new Action0() { + @Override + public void call() { + try { + d.unsubscribe(); + } finally { + w.unsubscribe(); + } + } + }, 550, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable e) { + err.set(e); + } + + @Override + public void onCompleted() { + + } + }); + + Assert.assertEquals(6, calls.get()); + Assert.assertNull(err.get()); + } + + @Test(timeout = 1000, expected = TestException.class) + public void repeatError() { + Completable c = error.completable.repeat(); + + c.await(); + } + + @Test(timeout = 1000) + public void repeat5Times() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + return null; + } + }).repeat(5); + + c.await(); + + Assert.assertEquals(5, calls.get()); + } + + @Test(timeout = 1000) + public void repeat1Time() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + return null; + } + }).repeat(1); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void repeat0Time() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + return null; + } + }).repeat(0); + + c.await(); + + Assert.assertEquals(0, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void repeatWhenNull() { + normal.completable.repeatWhen(null); + } + + @Test(timeout = 1000) + public void retryNormal() { + Completable c = normal.completable.retry(); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void retry5Times() { + final AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromAction(new Action0() { + @Override + public void call() { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + } + }).retry(); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void retryBiPredicate5Times() { + Completable c = error.completable.retry(new Func2() { + @Override + public Boolean call(Integer n, Throwable e) { + return n < 5; + } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void retryTimes5Error() { + Completable c = error.completable.retry(5); + + c.await(); + } + + @Test(timeout = 1000) + public void retryTimes5Normal() { + final AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromAction(new Action0() { + @Override + public void call() { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + } + }).retry(5); + + c.await(); + } + + @Test(expected = IllegalArgumentException.class) + public void retryNegativeTimes() { + normal.completable.retry(-1); + } + + @Test(timeout = 1000) + public void retryWhen5Times() { + final AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromAction(new Action0() { + @Override + public void call() { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + } + }).retryWhen(new Func1, Observable>() { + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Observable call(Observable o) { + return (Observable)o; + } + }); + + c.await(); + } + + @Test(timeout = 1000) + public void subscribe() throws InterruptedException { + final AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable + .delay(100, TimeUnit.MILLISECONDS) + .doOnComplete(new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + c.subscribe(); + + Thread.sleep(150); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void subscribeDispose() throws InterruptedException { + final AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable + .delay(200, TimeUnit.MILLISECONDS) + .doOnComplete(new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + Subscription d = c.subscribe(); + + Thread.sleep(100); + + d.unsubscribe(); + + Thread.sleep(150); + + Assert.assertFalse("Completed", complete.get()); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksNormal() { + final AtomicReference err = new AtomicReference(); + final AtomicBoolean complete = new AtomicBoolean(); + normal.completable.subscribe(new Action1() { + @Override + public void call(Throwable e) { + err.set(e); + } + }, new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + Assert.assertNull(err.get()); + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksError() { + final AtomicReference err = new AtomicReference(); + final AtomicBoolean complete = new AtomicBoolean(); + error.completable.subscribe(new Action1() { + @Override + public void call(Throwable e) { + err.set(e); + } + }, new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + Assert.assertTrue(err.get() instanceof TestException); + Assert.assertFalse("Not completed", complete.get()); + } + + @Test(expected = NullPointerException.class) + public void subscribeTwoCallbacksFirstNull() { + normal.completable.subscribe(null, new Action0() { + @Override + public void call() { } + }); + } + + @Test(expected = NullPointerException.class) + public void subscribeTwoCallbacksSecondNull() { + normal.completable.subscribe(null, new Action0() { + @Override + public void call() { } + }); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksCompleteThrows() { + final AtomicReference err = new AtomicReference(); + normal.completable.subscribe(new Action1() { + @Override + public void call(Throwable e) { + err.set(e); + } + }, new Action0() { + @Override + public void call() { throw new TestException(); } + }); + + Assert.assertTrue(String.valueOf(err.get()), err.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksOnErrorThrows() { + error.completable.subscribe(new Action1() { + @Override + public void call(Throwable e) { throw new TestException(); } + }, new Action0() { + @Override + public void call() { } + }); + } + + @Test(timeout = 1000) + public void subscribeActionNormal() { + final AtomicBoolean run = new AtomicBoolean(); + + normal.completable.subscribe(new Action0() { + @Override + public void call() { + run.set(true); + } + }); + + Assert.assertTrue("Not completed", run.get()); + } + + @Test(timeout = 1000) + public void subscribeActionError() { + final AtomicBoolean run = new AtomicBoolean(); + + error.completable.subscribe(new Action0() { + @Override + public void call() { + run.set(true); + } + }); + + Assert.assertFalse("Completed", run.get()); + } + + @Test(expected = NullPointerException.class) + public void subscribeActionNull() { + normal.completable.subscribe((Action0)null); + } + + @Test(expected = NullPointerException.class) + public void subscribeSubscriberNull() { + normal.completable.subscribe((Subscriber)null); + } + + @Test(expected = NullPointerException.class) + public void subscribeCompletableSubscriberNull() { + normal.completable.subscribe((CompletableSubscriber)null); + } + + @Test(timeout = 1000) + public void subscribeSubscriberNormal() { + TestSubscriber ts = new TestSubscriber(); + + normal.completable.subscribe(ts); + + ts.assertCompleted(); + ts.assertNoValues(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void subscribeSubscriberError() { + TestSubscriber ts = new TestSubscriber(); + + error.completable.subscribe(ts); + + ts.assertNotCompleted(); + ts.assertNoValues(); + ts.assertError(TestException.class); + } + + @Test(expected = NullPointerException.class) + public void subscribeOnNull() { + normal.completable.subscribeOn(null); + } + + @Test(timeout = 1000) + public void subscribeOnNormal() { + final AtomicReference name = new AtomicReference(); + + Completable c = Completable.create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + name.set(Thread.currentThread().getName()); + s.onSubscribe(Subscriptions.unsubscribed()); + s.onCompleted(); + } + }).subscribeOn(Schedulers.computation()); + + c.await(); + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void subscribeOnError() { + final AtomicReference name = new AtomicReference(); + + Completable c = Completable.create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber s) { + name.set(Thread.currentThread().getName()); + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(new TestException()); + } + }).subscribeOn(Schedulers.computation()); + + try { + c.await(); + Assert.fail("No exception thrown"); + } catch (TestException ex) { + // expected + } + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void timeoutEmitError() { + Throwable e = Completable.never().timeout(100, TimeUnit.MILLISECONDS).get(); + + Assert.assertTrue(e instanceof TimeoutException); + } + + @Test(timeout = 1000) + public void timeoutSwitchNormal() { + Completable c = Completable.never().timeout(100, TimeUnit.MILLISECONDS, normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void timeoutTimerCancelled() throws InterruptedException { + Completable c = Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + Thread.sleep(50); + return null; + } + }).timeout(100, TimeUnit.MILLISECONDS, normal.completable); + + c.await(); + + Thread.sleep(100); + + normal.assertSubscriptions(0); + } + + @Test(expected = NullPointerException.class) + public void timeoutUnitNull() { + normal.completable.timeout(1, null); + } + + @Test(expected = NullPointerException.class) + public void timeoutSchedulerNull() { + normal.completable.timeout(1, TimeUnit.SECONDS, (Scheduler)null); + } + + @Test(expected = NullPointerException.class) + public void timeoutOtherNull() { + normal.completable.timeout(1, TimeUnit.SECONDS, (Completable)null); + } + + @Test(timeout = 1000) + public void toNormal() { + Observable flow = normal.completable.to(new Func1>() { + @Override + public Observable call(Completable c) { + return c.toObservable(); + } + }); + + flow.toBlocking().forEach(new Action1(){ + @Override + public void call(Object e){ } + }); + } + + @Test(expected = NullPointerException.class) + public void toNull() { + normal.completable.to(null); + } + + @Test(timeout = 1000) + public void toObservableNormal() { + normal.completable.toObservable().toBlocking().forEach(new Action1() { + @Override + public void call(Object e) { } + }); + } + + @Test(timeout = 1000, expected = TestException.class) + public void toObservableError() { + error.completable.toObservable().toBlocking().forEach(new Action1() { + @Override + public void call(Object e) { } + }); + } + + static T get(Single single) { + final CountDownLatch cdl = new CountDownLatch(1); + + final AtomicReference v = new AtomicReference(); + final AtomicReference e = new AtomicReference(); + + single.subscribe(new SingleSubscriber() { + + @Override + public void onSuccess(T value) { + v.set(value); + cdl.countDown(); + } + + @Override + public void onError(Throwable error) { + e.set(error); + cdl.countDown(); + } + }); + + try { + cdl.await(); + } catch (InterruptedException ex) { + Exceptions.propagate(ex); + } + + if (e.get() != null) { + Exceptions.propagate(e.get()); + } + return v.get(); + } + + @Test(timeout = 1000) + public void toSingleSupplierNormal() { + int v = get(normal.completable.toSingle(new Func0() { + @Override + public Integer call() { + return 1; + } + })); + + Assert.assertEquals(1, v); + } + + @Test(timeout = 1000, expected = TestException.class) + public void toSingleSupplierError() { + get(error.completable.toSingle(new Func0() { + @Override + public Object call() { + return 1; + } + })); + } + + @Test(expected = NullPointerException.class) + public void toSingleSupplierNull() { + normal.completable.toSingle(null); + } + + @Test(expected = NullPointerException.class) + public void toSingleSupplierReturnsNull() { + get(normal.completable.toSingle(new Func0() { + @Override + public Object call() { + return null; + } + })); + } + + @Test(expected = TestException.class) + public void toSingleSupplierThrows() { + get(normal.completable.toSingle(new Func0() { + @Override + public Object call() { throw new TestException(); } + })); + } + + @Test(timeout = 1000, expected = TestException.class) + public void toSingleDefaultError() { + get(error.completable.toSingleDefault(1)); + } + + @Test(timeout = 1000) + public void toSingleDefaultNormal() { + Assert.assertEquals((Integer)1, get(normal.completable.toSingleDefault(1))); + } + + @Test(expected = NullPointerException.class) + public void toSingleDefaultNull() { + normal.completable.toSingleDefault(null); + } + + @Test(timeout = 1000) + public void unsubscribeOnNormal() throws InterruptedException { + final AtomicReference name = new AtomicReference(); + final CountDownLatch cdl = new CountDownLatch(1); + + normal.completable.delay(1, TimeUnit.SECONDS) + .doOnUnsubscribe(new Action0() { + @Override + public void call() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + }) + .unsubscribeOn(Schedulers.computation()) + .subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(final Subscription d) { + final Scheduler.Worker w = Schedulers.io().createWorker(); + + w.schedule(new Action0() { + @Override + public void call() { + try { + d.unsubscribe(); + } finally { + w.unsubscribe(); + } + } + }, 100, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onCompleted() { + + } + }); + + cdl.await(); + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(expected = NullPointerException.class) + public void ambArrayNull() { + Completable.amb((Completable[])null); + } + + @Test(timeout = 1000) + public void ambArrayEmpty() { + Completable c = Completable.amb(); + + c.await(); + } + + @Test(timeout = 1000) + public void ambArraySingleNormal() { + Completable c = Completable.amb(normal.completable); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void ambArraySingleError() { + Completable c = Completable.amb(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void ambArrayOneFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = Completable.amb(c1, c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps1.onCompleted(); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambArrayOneFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = Completable.amb(c1, c2); + + final AtomicReference complete = new AtomicReference(); + + c.subscribe(new Action1() { + @Override + public void call(Throwable e) { + complete.set(e); + } + }, new Action0() { + @Override + public void call() { } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps1.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void ambArraySecondFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = Completable.amb(c1, c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps2.onCompleted(); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambArraySecondFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = Completable.amb(c1, c2); + + final AtomicReference complete = new AtomicReference(); + + c.subscribe(new Action1() { + @Override + public void call(Throwable e) { + complete.set(e); + } + }, new Action0() { + @Override + public void call() { } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps2.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void ambMultipleOneIsNull() { + Completable c = Completable.amb(null, normal.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void ambIterableEmpty() { + Completable c = Completable.amb(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void ambIterableNull() { + Completable.amb((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void ambIterableIteratorNull() { + Completable c = Completable.amb(new Iterable() { + @Override + public Iterator iterator() { + return null; + } + }); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void ambIterableWithNull() { + Completable c = Completable.amb(Arrays.asList(null, normal.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void ambIterableSingle() { + Completable c = Completable.amb(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void ambIterableMany() { + Completable c = Completable.amb(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void ambIterableOneThrows() { + Completable c = Completable.amb(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void ambIterableManyOneThrows() { + Completable c = Completable.amb(Arrays.asList(error.completable, normal.completable)); + + c.await(); + } + + @Test(expected = TestException.class) + public void ambIterableIterableThrows() { + Completable c = Completable.amb(new Iterable() { + @Override + public Iterator iterator() { + throw new TestException(); + } + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void ambIterableIteratorHasNextThrows() { + Completable c = Completable.amb(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void ambIterableIteratorNextThrows() { + Completable c = Completable.amb(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void ambWithNull() { + normal.completable.ambWith(null); + } + + @Test(timeout = 1000) + public void ambWithArrayOneFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = c1.ambWith(c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps1.onCompleted(); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambWithArrayOneFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = c1.ambWith(c2); + + final AtomicReference complete = new AtomicReference(); + + c.subscribe(new Action1() { + @Override + public void call(Throwable e) { + complete.set(e); + } + }, new Action0() { + @Override + public void call() { } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps1.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void ambWithArraySecondFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = c1.ambWith(c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action0() { + @Override + public void call() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps2.onCompleted(); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambWithArraySecondFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromObservable(ps1); + + Completable c2 = Completable.fromObservable(ps2); + + Completable c = c1.ambWith(c2); + + final AtomicReference complete = new AtomicReference(); + + c.subscribe(new Action1() { + @Override + public void call(Throwable e) { + complete.set(e); + } + }, new Action0() { + @Override + public void call() { } + }); + + Assert.assertTrue("First subject no subscribers", ps1.hasObservers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasObservers()); + + ps2.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasObservers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasObservers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void startWithCompletableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Completable c = normal.completable + .startWith(Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return null; + } + })); + + c.await(); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void startWithCompletableError() { + Completable c = normal.completable.startWith(error.completable); + + try { + c.await(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + normal.assertSubscriptions(0); + error.assertSubscriptions(1); + } + } + + @Test(timeout = 1000) + public void startWithFlowableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Observable c = normal.completable + .startWith(Observable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return 1; + } + })); + + TestSubscriber ts = new TestSubscriber(); + + c.subscribe(ts); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void startWithFlowableError() { + Observable c = normal.completable + .startWith(Observable.error(new TestException())); + + TestSubscriber ts = new TestSubscriber(); + + c.subscribe(ts); + + normal.assertSubscriptions(0); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test(expected = NullPointerException.class) + public void startWithCompletableNull() { + normal.completable.startWith((Completable)null); + } + + @Test(expected = NullPointerException.class) + public void startWithFlowableNull() { + normal.completable.startWith((Observable)null); + } + + @Test(expected = NullPointerException.class) + public void endWithCompletableNull() { + normal.completable.endWith((Completable)null); + } + + @Test(expected = NullPointerException.class) + public void endWithFlowableNull() { + normal.completable.endWith((Observable)null); + } + + @Test(timeout = 1000) + public void endWithCompletableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Completable c = normal.completable + .endWith(Completable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return null; + } + })); + + c.await(); + + Assert.assertFalse("Start with other", run.get()); + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void endWithCompletableError() { + Completable c = normal.completable.endWith(error.completable); + + try { + c.await(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + normal.assertSubscriptions(1); + error.assertSubscriptions(1); + } + } + + @Test(timeout = 1000) + public void endWithFlowableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Observable c = normal.completable + .endWith(Observable.fromCallable(new Callable() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return 1; + } + })); + + TestSubscriber ts = new TestSubscriber(); + + c.subscribe(ts); + + Assert.assertFalse("Start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void endWithFlowableError() { + Observable c = normal.completable + .endWith(Observable.error(new TestException())); + + TestSubscriber ts = new TestSubscriber(); + + c.subscribe(ts); + + normal.assertSubscriptions(1); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } +} \ No newline at end of file From e41b215c64637658defaf8a625124bb5332574b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 14 Oct 2015 20:55:09 +0200 Subject: [PATCH 414/857] 1.x: operator DelaySubscription with plain Observable --- src/main/java/rx/Observable.java | 26 ++ .../OnSubscribeDelaySubscriptionOther.java | 79 ++++++ ...OnSubscribeDelaySubscriptionOtherTest.java | 246 ++++++++++++++++++ 3 files changed, 351 insertions(+) create mode 100644 src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java create mode 100644 src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 0a2ebab8ce..400f07416f 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4195,6 +4195,32 @@ public final Observable delaySubscription(Func0> return create(new OnSubscribeDelaySubscriptionWithSelector(this, subscriptionDelay)); } + /** + * Returns an Observable that delays the subscription to this Observable + * until the other Observable emits an element or completes normally. + *

+ *

+ *
Backpressure:
+ *
The operator forwards the backpressure requests to this Observable once + * the subscription happens and requests Long.MAX_VALUE from the other Observable
+ *
Scheduler:
+ *
This method does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param the value type of the other Observable, irrelevant + * @param other the other Observable that should trigger the subscription + * to this Observable. + * @return an Observable that delays the subscription to this Observable + * until the other Observable emits an element or completes normally. + */ + @Experimental + public final Observable delaySubscription(Observable other) { + if (other == null) { + throw new NullPointerException(); + } + return create(new OnSubscribeDelaySubscriptionOther(this, other)); + } + /** * Returns an Observable that reverses the effect of {@link #materialize materialize} by transforming the * {@link Notification} objects emitted by the source Observable into the items or notifications they diff --git a/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java new file mode 100644 index 0000000000..2a8b7e1601 --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeDelaySubscriptionOther.java @@ -0,0 +1,79 @@ +/** + * 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.*; +import rx.Observable.OnSubscribe; +import rx.observers.Subscribers; +import rx.plugins.*; +import rx.subscriptions.SerialSubscription; + +/** + * Delays the subscription to the main source until the other + * observable fires an event or completes. + * @param the main type + * @param the other value type, ignored + */ +public final class OnSubscribeDelaySubscriptionOther implements OnSubscribe { + final Observable main; + final Observable other; + + public OnSubscribeDelaySubscriptionOther(Observable main, Observable other) { + this.main = main; + this.other = other; + } + + @Override + public void call(Subscriber t) { + final Subscriber child = Subscribers.wrap(t); + + final SerialSubscription serial = new SerialSubscription(); + + Subscriber otherSubscriber = new Subscriber() { + boolean done; + @Override + public void onNext(U 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.unsafeSubscribe(child); + } + }; + + serial.set(otherSubscriber); + + other.unsafeSubscribe(otherSubscriber); + } +} diff --git a/src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java b/src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java new file mode 100644 index 0000000000..e157a788e5 --- /dev/null +++ b/src/test/java/rx/internal/operators/OnSubscribeDelaySubscriptionOtherTest.java @@ -0,0 +1,246 @@ +/** + * 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.atomic.AtomicInteger; + +import org.junit.*; + +import rx.Observable; +import rx.exceptions.TestException; +import rx.functions.Action0; +import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; + +public class OnSubscribeDelaySubscriptionOtherTest { + @Test + public void testNoPrematureSubscription() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.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 testNoMultipleSubscriptions() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.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); + other.onNext(2); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testCompleteTriggersSubscription() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.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.onCompleted(); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testNoPrematureSubscriptionToError() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.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.onCompleted(); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } + + @Test + public void testNoSubscriptionIfOtherErrors() { + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.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); + } + + @Test + public void testBackpressurePassesThrough() { + + PublishSubject other = PublishSubject.create(); + + TestSubscriber ts = TestSubscriber.create(0L); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.just(1, 2, 3, 4, 5) + .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()); + + Assert.assertFalse("Not unsubscribed from other", other.hasObservers()); + + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertNoValues(); + + ts.requestMore(1); + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(10); + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertCompleted(); + } +} From 019821779964641a2e302f86545207660887c918 Mon Sep 17 00:00:00 2001 From: Ho Yan Leung Date: Wed, 14 Oct 2015 21:16:44 -0700 Subject: [PATCH 415/857] Removes unused(?) source field in OperatorDelay --- src/main/java/rx/Observable.java | 2 +- src/main/java/rx/internal/operators/OperatorDelay.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 0a2ebab8ce..fbf0300733 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4127,7 +4127,7 @@ public final Observable delay(long delay, TimeUnit unit) { * @see ReactiveX operators documentation: Delay */ public final Observable delay(long delay, TimeUnit unit, Scheduler scheduler) { - return lift(new OperatorDelay(this, delay, unit, scheduler)); + return lift(new OperatorDelay(delay, unit, scheduler)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorDelay.java b/src/main/java/rx/internal/operators/OperatorDelay.java index 00ab5d1b49..4c0172f692 100644 --- a/src/main/java/rx/internal/operators/OperatorDelay.java +++ b/src/main/java/rx/internal/operators/OperatorDelay.java @@ -32,13 +32,11 @@ */ public final class OperatorDelay implements Operator { - final Observable source; final long delay; final TimeUnit unit; final Scheduler scheduler; - public OperatorDelay(Observable source, long delay, TimeUnit unit, Scheduler scheduler) { - this.source = source; + public OperatorDelay(long delay, TimeUnit unit, Scheduler scheduler) { this.delay = delay; this.unit = unit; this.scheduler = scheduler; From a596f0f42851c8e6566d69119ada5b6d1235afa2 Mon Sep 17 00:00:00 2001 From: Ho Yan Leung Date: Tue, 13 Oct 2015 20:34:04 -0700 Subject: [PATCH 416/857] Adds delay operator to Single This commit adds the `delay(long delay, TimeUnit unit, Scheduler scheduler)` and `delay(long delay, TimeUnit unit)` operators to `rx.Single`. --- src/main/java/rx/Single.java | 48 ++++++++++++++++++++++++++++++++ src/test/java/rx/SingleTest.java | 45 ++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 6817a4e283..e082daeaab 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -21,6 +21,7 @@ import rx.annotations.Experimental; import rx.exceptions.Exceptions; import rx.exceptions.OnErrorNotImplementedException; +import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; @@ -32,6 +33,7 @@ import rx.functions.Func8; import rx.functions.Func9; import rx.internal.operators.OnSubscribeToObservableFuture; +import rx.internal.operators.OperatorDelay; import rx.internal.operators.OperatorDoOnEach; import rx.internal.operators.OperatorMap; import rx.internal.operators.OperatorObserveOn; @@ -1898,4 +1900,50 @@ public void onNext(T t) { return lift(new OperatorDoOnEach(observer)); } + + /** + * 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. + *

+ * + *

+ *
Scheduler:
+ *
you specify which {@link Scheduler} this operator will use
+ *
+ * + * @param delay + * the delay to shift the source by + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@link Scheduler} to use for delaying + * @return the source Single shifted in time by the specified delay + * @see ReactiveX operators documentation: Delay + */ + @Experimental + public final Single delay(long delay, TimeUnit unit, Scheduler scheduler) { + return lift(new OperatorDelay(delay, unit, scheduler)); + } + + /** + * 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 Observable are not delayed. + *

+ * + *

+ *
Scheduler:
+ *
This version of {@code delay} operates by default on the {@code compuation} {@link Scheduler}.
+ *
+ * + * @param delay + * the delay to shift the source by + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @return the source Single shifted in time by the specified delay + * @see ReactiveX operators documentation: Delay + */ + @Experimental + public final Single delay(long delay, TimeUnit unit) { + return delay(delay, unit, Schedulers.computation()); + } } diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index de1a38f0ca..bba4d09bc7 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -39,10 +39,12 @@ import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; +import rx.schedulers.TestScheduler; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; + public class SingleTest { @Test @@ -436,7 +438,7 @@ public void call() { fail("timed out waiting for latch"); } } - + @Test public void testBackpressureAsObservable() { Single s = Single.create(new OnSubscribe() { @@ -462,7 +464,7 @@ public void onStart() { ts.assertValue("hello"); } - + @Test public void testToObservable() { Observable a = Single.just("a").toObservable(); @@ -648,4 +650,43 @@ public void doOnSuccessShouldNotSwallowExceptionThrownByAction() { verify(action).call(eq("value")); } + + @Test + public void delayWithSchedulerShouldDelayCompletion() { + TestScheduler scheduler = new TestScheduler(); + Single single = Single.just(1).delay(100, TimeUnit.DAYS, scheduler); + + TestSubscriber subscriber = new TestSubscriber(); + single.subscribe(subscriber); + + subscriber.assertNotCompleted(); + scheduler.advanceTimeBy(99, TimeUnit.DAYS); + subscriber.assertNotCompleted(); + scheduler.advanceTimeBy(91, TimeUnit.DAYS); + subscriber.assertCompleted(); + subscriber.assertValue(1); + } + + @Test + public void delayWithSchedulerShouldShortCutWithFailure() { + TestScheduler scheduler = new TestScheduler(); + final RuntimeException expected = new RuntimeException(); + Single single = Single.create(new OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + singleSubscriber.onSuccess(1); + singleSubscriber.onError(expected); + } + }).delay(100, TimeUnit.DAYS, scheduler); + + TestSubscriber subscriber = new TestSubscriber(); + single.subscribe(subscriber); + + subscriber.assertNotCompleted(); + scheduler.advanceTimeBy(99, TimeUnit.DAYS); + subscriber.assertNotCompleted(); + scheduler.advanceTimeBy(91, TimeUnit.DAYS); + subscriber.assertNoValues(); + subscriber.assertError(expected); + } } From cba5952eaeea168d6013b879ac9093b2fe2f43d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Fri, 16 Oct 2015 22:34:54 +0200 Subject: [PATCH 417/857] 1.x: fix: bounded replay() not requesting enough for latecommers --- .../rx/internal/operators/OperatorReplay.java | 40 ++++-- .../operators/OperatorReplayTest.java | 117 ++++++++++++++++-- 2 files changed, 142 insertions(+), 15 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorReplay.java b/src/main/java/rx/internal/operators/OperatorReplay.java index 93d78ee14b..a76f2f3c0b 100644 --- a/src/main/java/rx/internal/operators/OperatorReplay.java +++ b/src/main/java/rx/internal/operators/OperatorReplay.java @@ -221,6 +221,10 @@ public void call(Subscriber child) { // the producer has been registered with the current subscriber-to-source so // at least it will receive the next terminal event child.add(inner); + + // pin the head of the buffer here, shouldn't affect anything else + r.buffer.replay(inner); + // setting the producer will trigger the first request to be considered by // the subscriber-to-source. child.setProducer(inner); @@ -858,9 +862,15 @@ public void replay(InnerProducer output) { static final class Node extends AtomicReference { /** */ private static final long serialVersionUID = 245354315435971818L; + + /** The contained value. */ final Object value; - public Node(Object value) { + /** The absolute index of the value. */ + final long index; + + public Node(Object value, long index) { this.value = value; + this.index = index; } } @@ -878,9 +888,12 @@ static class BoundedReplayBuffer extends AtomicReference implements Rep Node tail; int size; + /** The total number of received values so far. */ + long index; + public BoundedReplayBuffer() { nl = NotificationLite.instance(); - Node n = new Node(null); + Node n = new Node(null, 0); tail = n; set(n); } @@ -929,7 +942,7 @@ final void setFirst(Node n) { @Override public final void next(T value) { Object o = enterTransform(nl.next(value)); - Node n = new Node(o); + Node n = new Node(o, ++index); addLast(n); truncate(); } @@ -937,7 +950,7 @@ public final void next(T value) { @Override public final void error(Throwable e) { Object o = enterTransform(nl.error(e)); - Node n = new Node(o); + Node n = new Node(o, ++index); addLast(n); truncateFinal(); } @@ -945,7 +958,7 @@ public final void error(Throwable e) { @Override public final void complete() { Object o = enterTransform(nl.completed()); - Node n = new Node(o); + Node n = new Node(o, ++index); addLast(n); truncateFinal(); } @@ -965,15 +978,25 @@ public final void replay(InnerProducer output) { } long r = output.get(); - long r0 = r; + boolean unbounded = r == Long.MAX_VALUE; long e = 0L; Node node = output.index(); if (node == null) { node = get(); output.index = node; + + /* + * Since this is a latecommer, fix its total requested amount + * as if it got all the values up to the node.index + */ + output.addTotalRequested(node.index); } - + + if (output.isUnsubscribed()) { + return; + } + while (r != 0) { Node v = node.get(); if (v != null) { @@ -993,6 +1016,7 @@ public final void replay(InnerProducer output) { return; } e++; + r--; node = v; } else { break; @@ -1004,7 +1028,7 @@ public final void replay(InnerProducer output) { if (e != 0L) { output.index = node; - if (r0 != Long.MAX_VALUE) { + if (!unbounded) { output.produced(e); } } diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index c0ec384d84..3da35b83b8 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -749,11 +749,11 @@ public boolean isUnsubscribed() { @Test public void testBoundedReplayBuffer() { BoundedReplayBuffer buf = new BoundedReplayBuffer(); - buf.addLast(new Node(1)); - buf.addLast(new Node(2)); - buf.addLast(new Node(3)); - buf.addLast(new Node(4)); - buf.addLast(new Node(5)); + buf.addLast(new Node(1, 0)); + buf.addLast(new Node(2, 1)); + buf.addLast(new Node(3, 2)); + buf.addLast(new Node(4, 3)); + buf.addLast(new Node(5, 4)); List values = new ArrayList(); buf.collect(values); @@ -768,8 +768,8 @@ public void testBoundedReplayBuffer() { buf.collect(values); Assert.assertTrue(values.isEmpty()); - buf.addLast(new Node(5)); - buf.addLast(new Node(6)); + buf.addLast(new Node(5, 5)); + buf.addLast(new Node(6, 6)); buf.collect(values); Assert.assertEquals(Arrays.asList(5, 6), values); @@ -1145,4 +1145,107 @@ public void call(Long t) { Assert.assertEquals(Arrays.asList(5L, 5L), requests); } + @Test + public void testSubscribersComeAndGoAtRequestBoundaries() { + ConnectableObservable source = Observable.range(1, 10).replay(1); + source.connect(); + + TestSubscriber ts1 = TestSubscriber.create(2); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.unsubscribe(); + + TestSubscriber ts2 = TestSubscriber.create(2); + + source.subscribe(ts2); + + ts2.assertValues(2, 3); + ts2.assertNoErrors(); + ts2.unsubscribe(); + + TestSubscriber ts21 = TestSubscriber.create(1); + + source.subscribe(ts21); + + ts21.assertValues(3); + ts21.assertNoErrors(); + ts21.unsubscribe(); + + TestSubscriber ts22 = TestSubscriber.create(1); + + source.subscribe(ts22); + + ts22.assertValues(3); + ts22.assertNoErrors(); + ts22.unsubscribe(); + + + TestSubscriber ts3 = TestSubscriber.create(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.getOnNextEvents()); + ts3.assertValues(3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertCompleted(); + } + + @Test + public void testSubscribersComeAndGoAtRequestBoundaries2() { + ConnectableObservable source = Observable.range(1, 10).replay(2); + source.connect(); + + TestSubscriber ts1 = TestSubscriber.create(2); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.unsubscribe(); + + TestSubscriber ts11 = TestSubscriber.create(2); + + source.subscribe(ts11); + + ts11.assertValues(1, 2); + ts11.assertNoErrors(); + ts11.unsubscribe(); + + TestSubscriber ts2 = TestSubscriber.create(3); + + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3); + ts2.assertNoErrors(); + ts2.unsubscribe(); + + TestSubscriber ts21 = TestSubscriber.create(1); + + source.subscribe(ts21); + + ts21.assertValues(2); + ts21.assertNoErrors(); + ts21.unsubscribe(); + + TestSubscriber ts22 = TestSubscriber.create(1); + + source.subscribe(ts22); + + ts22.assertValues(2); + ts22.assertNoErrors(); + ts22.unsubscribe(); + + + TestSubscriber ts3 = TestSubscriber.create(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.getOnNextEvents()); + ts3.assertValues(2, 3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertCompleted(); + } } \ No newline at end of file From 974b651dcae927d26b53b150316c602721bfe233 Mon Sep 17 00:00:00 2001 From: konmik Date: Sat, 17 Oct 2015 00:40:37 +0300 Subject: [PATCH 418/857] OnErrorFailedException fix --- src/main/java/rx/Observable.java | 8 +-- src/main/java/rx/exceptions/Exceptions.java | 7 +- .../java/rx/exceptions/ExceptionsTest.java | 72 +++++++++++++++++-- 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 0a2ebab8ce..0acb0e9b2e 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -164,15 +164,11 @@ public void call(Subscriber o) { // 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 - if (e instanceof OnErrorNotImplementedException) { - throw (OnErrorNotImplementedException) e; - } + Exceptions.throwIfFatal(e); st.onError(e); } } catch (Throwable e) { - if (e instanceof OnErrorNotImplementedException) { - throw (OnErrorNotImplementedException) 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); diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index a0439028eb..4701e2bb5f 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -76,12 +76,7 @@ public static void throwIfFatal(Throwable t) { if (t instanceof OnErrorNotImplementedException) { throw (OnErrorNotImplementedException) t; } else if (t instanceof OnErrorFailedException) { - Throwable cause = t.getCause(); - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else { - throw (OnErrorFailedException) t; - } + throw (OnErrorFailedException) t; } // values here derived from https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495 else if (t instanceof StackOverflowError) { diff --git a/src/test/java/rx/exceptions/ExceptionsTest.java b/src/test/java/rx/exceptions/ExceptionsTest.java index 4148f1b9e6..96396ccb75 100644 --- a/src/test/java/rx/exceptions/ExceptionsTest.java +++ b/src/test/java/rx/exceptions/ExceptionsTest.java @@ -25,6 +25,8 @@ import rx.Observable; import rx.Observer; import rx.functions.Action1; +import rx.functions.Func1; +import rx.observables.GroupedObservable; import rx.subjects.PublishSubject; public class ExceptionsTest { @@ -45,7 +47,7 @@ public void call(Integer t1) { public void testStackOverflowWouldOccur() { final PublishSubject a = PublishSubject.create(); final PublishSubject b = PublishSubject.create(); - final int MAX_STACK_DEPTH = 1000; + final int MAX_STACK_DEPTH = 800; final AtomicInteger depth = new AtomicInteger(); a.subscribe(new Observer() { @@ -156,10 +158,72 @@ public void onNext(Object o) { } }); fail("expecting an exception to be thrown"); - } catch (CompositeException t) { - assertTrue(t.getExceptions().get(0) instanceof IllegalArgumentException); - assertTrue(t.getExceptions().get(1) instanceof IllegalStateException); + } catch (OnErrorFailedException t) { + CompositeException cause = (CompositeException) t.getCause(); + assertTrue(cause.getExceptions().get(0) instanceof IllegalArgumentException); + assertTrue(cause.getExceptions().get(1) instanceof IllegalStateException); } } + /** + * https://github.com/ReactiveX/RxJava/issues/2998 + */ + @Test(expected = OnErrorFailedException.class) + public void testOnErrorExceptionIsThrownFromGroupBy() throws Exception { + Observable + .just(1) + .groupBy(new Func1() { + @Override + public Integer call(Integer integer) { + throw new RuntimeException(); + } + }) + .subscribe(new Observer>() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(); + } + + @Override + public void onNext(GroupedObservable integerIntegerGroupedObservable) { + + } + }); + } + + /** + * https://github.com/ReactiveX/RxJava/issues/2998 + */ + @Test(expected = OnErrorFailedException.class) + public void testOnErrorExceptionIsThrownFromOnNext() throws Exception { + Observable + .just(1) + .doOnNext(new Action1() { + @Override + public void call(Integer integer) { + throw new RuntimeException(); + } + }) + .subscribe(new Observer() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(); + } + + @Override + public void onNext(Integer integer) { + + } + }); + } } From 4c33811a4de52887d99a44ef7494c121edc69c36 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sat, 17 Oct 2015 05:16:52 +0300 Subject: [PATCH 419/857] Clarify contracts of CompositeSubscription in its javadoc --- .../java/rx/subscriptions/CompositeSubscription.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/subscriptions/CompositeSubscription.java b/src/main/java/rx/subscriptions/CompositeSubscription.java index d275d7ebfb..f4941875f5 100644 --- a/src/main/java/rx/subscriptions/CompositeSubscription.java +++ b/src/main/java/rx/subscriptions/CompositeSubscription.java @@ -27,6 +27,8 @@ /** * Subscription that represents a group of Subscriptions that are unsubscribed together. + *

+ * All methods of this class are thread-safe. */ public final class CompositeSubscription implements Subscription { @@ -98,8 +100,8 @@ public void remove(final Subscription s) { /** * Unsubscribes any subscriptions that are currently part of this {@code CompositeSubscription} and remove - * them from the {@code CompositeSubscription} so that the {@code CompositeSubscription} is empty and in - * an unoperative state. + * them from the {@code CompositeSubscription} so that the {@code CompositeSubscription} is empty and + * able to manage new subscriptions. */ public void clear() { if (!unsubscribed) { @@ -116,6 +118,11 @@ public void clear() { } } + /** + * Unsubscribes itself and all inner subscriptions. + *

After call of this method, new {@code Subscription}s added to {@link CompositeSubscription} + * will be unsubscribed immediately. + */ @Override public void unsubscribe() { if (!unsubscribed) { From e3bb040ec2b16f5d5c82b24079ca644c0a00eedb Mon Sep 17 00:00:00 2001 From: zsxwing Date: Tue, 20 Oct 2015 00:46:01 +0800 Subject: [PATCH 420/857] A minor doc fix for `interval` --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index fbf0300733..1005823ecd 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1330,7 +1330,7 @@ public final static Observable interval(long interval, TimeUnit unit, Sche *

This operator does not support backpressure as it uses time. If the downstream needs a slower rate * it should slow the timer or use something like {@link #onBackpressureDrop}.
*
Scheduler:
- *
{@code timer} operates by default on the {@code computation} {@link Scheduler}.
+ *
{@code interval} operates by default on the {@code computation} {@link Scheduler}.
* * * @param initialDelay From e2b234a8b95041b0686a0e0b546f454af496ce84 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Sat, 24 Oct 2015 13:11:42 +0800 Subject: [PATCH 421/857] Fix other places that may swallow OnErrorFailedException --- src/main/java/rx/Observable.java | 8 +- src/main/java/rx/Single.java | 18 +-- .../java/rx/exceptions/ExceptionsTest.java | 110 ++++++++++++++++++ 3 files changed, 117 insertions(+), 19 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 444f20a4d8..cf1686ad83 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -8176,10 +8176,8 @@ public final Subscription unsafeSubscribe(Subscriber subscriber) { // if an unhandled error occurs executing the onSubscribe we will propagate it try { subscriber.onError(hook.onSubscribeError(e)); - } catch (OnErrorNotImplementedException e2) { - // special handling when onError is not implemented ... we just rethrow - throw e2; } catch (Throwable e2) { + Exceptions.throwIfFatal(e2); // if this happens it means the onError itself failed (perhaps an invalid function implementation) // so we are unable to propagate the error correctly and will just throw RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2); @@ -8271,10 +8269,8 @@ private static Subscription subscribe(Subscriber subscriber, Obse // if an unhandled error occurs executing the onSubscribe we will propagate it try { subscriber.onError(hook.onSubscribeError(e)); - } catch (OnErrorNotImplementedException e2) { - // special handling when onError is not implemented ... we just rethrow - throw e2; } catch (Throwable e2) { + Exceptions.throwIfFatal(e2); // if this happens it means the onError itself failed (perhaps an invalid function implementation) // so we are unable to propagate the error correctly and will just throw RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2); diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index e082daeaab..77e644bc3d 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -190,18 +190,14 @@ public void call(Subscriber o) { st.onStart(); onSubscribe.call(st); } catch (Throwable e) { - // localized capture of errors rather than it skipping all operators + Exceptions.throwIfFatal(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 - if (e instanceof OnErrorNotImplementedException) { - throw (OnErrorNotImplementedException) e; - } st.onError(e); } } catch (Throwable e) { - if (e instanceof OnErrorNotImplementedException) { - throw (OnErrorNotImplementedException) 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); @@ -1507,10 +1503,8 @@ public final void unsafeSubscribe(Subscriber subscriber) { // if an unhandled error occurs executing the onSubscribe we will propagate it try { subscriber.onError(hook.onSubscribeError(e)); - } catch (OnErrorNotImplementedException e2) { - // special handling when onError is not implemented ... we just rethrow - throw e2; } catch (Throwable e2) { + Exceptions.throwIfFatal(e2); // if this happens it means the onError itself failed (perhaps an invalid function implementation) // so we are unable to propagate the error correctly and will just throw RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2); @@ -1596,10 +1590,8 @@ public final Subscription subscribe(Subscriber subscriber) { // if an unhandled error occurs executing the onSubscribe we will propagate it try { subscriber.onError(hook.onSubscribeError(e)); - } catch (OnErrorNotImplementedException e2) { - // special handling when onError is not implemented ... we just rethrow - throw e2; } catch (Throwable e2) { + Exceptions.throwIfFatal(e2); // if this happens it means the onError itself failed (perhaps an invalid function implementation) // so we are unable to propagate the error correctly and will just throw RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2); diff --git a/src/test/java/rx/exceptions/ExceptionsTest.java b/src/test/java/rx/exceptions/ExceptionsTest.java index 96396ccb75..5906a6d6f9 100644 --- a/src/test/java/rx/exceptions/ExceptionsTest.java +++ b/src/test/java/rx/exceptions/ExceptionsTest.java @@ -22,6 +22,9 @@ import org.junit.Test; +import rx.Single; +import rx.SingleSubscriber; +import rx.Subscriber; import rx.Observable; import rx.Observer; import rx.functions.Action1; @@ -226,4 +229,111 @@ public void onNext(Integer integer) { } }); } + + @Test(expected = OnErrorFailedException.class) + public void testOnErrorExceptionIsThrownFromSubscribe() { + Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber s1) { + Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber s2) { + throw new IllegalArgumentException("original exception"); + } + }).subscribe(s1); + } + } + ).subscribe(new OnErrorFailedSubscriber()); + } + + @Test(expected = OnErrorFailedException.class) + public void testOnErrorExceptionIsThrownFromUnsafeSubscribe() { + Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber s1) { + Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber s2) { + throw new IllegalArgumentException("original exception"); + } + }).unsafeSubscribe(s1); + } + } + ).subscribe(new OnErrorFailedSubscriber()); + } + + @Test(expected = OnErrorFailedException.class) + public void testOnErrorExceptionIsThrownFromSingleDoOnSuccess() throws Exception { + Single.just(1) + .doOnSuccess(new Action1() { + @Override + public void call(Integer integer) { + throw new RuntimeException(); + } + }) + .subscribe(new OnErrorFailedSubscriber()); + } + + @Test(expected = OnErrorFailedException.class) + public void testOnErrorExceptionIsThrownFromSingleSubscribe() { + Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber s1) { + Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber s2) { + throw new IllegalArgumentException("original exception"); + } + }).subscribe(s1); + } + } + ).subscribe(new OnErrorFailedSubscriber()); + } + + @Test(expected = OnErrorFailedException.class) + public void testOnErrorExceptionIsThrownFromSingleUnsafeSubscribe() { + Single.create(new Single.OnSubscribe() { + @Override + public void call(final SingleSubscriber s1) { + Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber s2) { + throw new IllegalArgumentException("original exception"); + } + }).unsafeSubscribe(new Subscriber() { + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + s1.onError(e); + } + + @Override + public void onNext(Integer v) { + s1.onSuccess(v); + } + + }); + } + } + ).subscribe(new OnErrorFailedSubscriber()); + } + + private class OnErrorFailedSubscriber extends Subscriber { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(); + } + + @Override + public void onNext(Integer value) { + } + } } From 8bcbeb5bf427140334c3e86f0ddb453ba3ce2777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eug=C3=AAnio=20Cabral?= Date: Wed, 28 Oct 2015 12:01:29 +1030 Subject: [PATCH 422/857] Fix indentation --- src/main/java/rx/Notification.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/Notification.java b/src/main/java/rx/Notification.java index b708b58766..1aa528b5a8 100644 --- a/src/main/java/rx/Notification.java +++ b/src/main/java/rx/Notification.java @@ -203,10 +203,10 @@ public boolean equals(Object obj) { if (hasThrowable() && !getThrowable().equals(notification.getThrowable())) return false; if(!hasValue() && !hasThrowable() && notification.hasValue()) - return false; + return false; if(!hasValue() && !hasThrowable() && notification.hasThrowable()) - return false; - + return false; + return true; } } From 94beabb5d7dd6bc1ae4ecd0388f4c26bfa13459b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eug=C3=AAnio=20Cabral?= Date: Wed, 28 Oct 2015 19:07:00 +1030 Subject: [PATCH 423/857] Add brackets to the 'if's --- src/main/java/rx/Notification.java | 31 ++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/Notification.java b/src/main/java/rx/Notification.java index 1aa528b5a8..a35cf6e60a 100644 --- a/src/main/java/rx/Notification.java +++ b/src/main/java/rx/Notification.java @@ -189,23 +189,38 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj == null) + if (obj == null) { return false; - if (this == obj) + } + + if (this == obj) { return true; - if (obj.getClass() != getClass()) + } + + if (obj.getClass() != getClass()) { return false; + } + Notification notification = (Notification) obj; - if (notification.getKind() != getKind()) + if (notification.getKind() != getKind()) { return false; - if (hasValue() && !getValue().equals(notification.getValue())) + } + + if (hasValue() && !getValue().equals(notification.getValue())) { return false; - if (hasThrowable() && !getThrowable().equals(notification.getThrowable())) + } + + if (hasThrowable() && !getThrowable().equals(notification.getThrowable())) { return false; - if(!hasValue() && !hasThrowable() && notification.hasValue()) + } + + if (!hasValue() && !hasThrowable() && notification.hasValue()) { return false; - if(!hasValue() && !hasThrowable() && notification.hasThrowable()) + } + + if (!hasValue() && !hasThrowable() && notification.hasThrowable()) { return false; + } return true; } From 4b07cd3c576038a399e974b762877262c6bcdc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 28 Oct 2015 18:54:40 +0100 Subject: [PATCH 424/857] 1.x: benchmark range + flatMap throughput. --- .../java/rx/operators/FlatMapRangePerf.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/perf/java/rx/operators/FlatMapRangePerf.java diff --git a/src/perf/java/rx/operators/FlatMapRangePerf.java b/src/perf/java/rx/operators/FlatMapRangePerf.java new file mode 100644 index 0000000000..e8d58795b7 --- /dev/null +++ b/src/perf/java/rx/operators/FlatMapRangePerf.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.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 typical atomic operations on volatile fields and AtomicXYZ classes. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*FlatMapRangePerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class FlatMapRangePerf { + @Param({ "1", "10", "1000", "1000000" }) + public int times; + + Observable rangeFlatMapJust; + Observable rangeFlatMapRange; + + @Setup + public void setup() { + Observable range = Observable.range(1, times); + + rangeFlatMapJust = range.flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.just(v); + } + }); + rangeFlatMapRange = range.flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }); + } + + @Benchmark + public void rangeFlatMapJust(Blackhole bh) { + rangeFlatMapJust.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void rangeFlatMapRange(Blackhole bh) { + rangeFlatMapRange.subscribe(new LatchedObserver(bh)); + } + +} From 0e45a7efae4583b9bc9d82af53407ec274dd48b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 28 Oct 2015 22:02:51 +0100 Subject: [PATCH 425/857] 1.x: add a source OnSubscribe which works from an array directly --- src/main/java/rx/Observable.java | 43 +++--- .../operators/OnSubscribeFromArray.java | 128 ++++++++++++++++++ .../java/rx/operators/FromComparison.java | 123 +++++++++++++++++ .../operators/OnSubscribeFromArrayTest.java | 67 +++++++++ 4 files changed, 343 insertions(+), 18 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OnSubscribeFromArray.java create mode 100644 src/perf/java/rx/operators/FromComparison.java create mode 100644 src/test/java/rx/internal/operators/OnSubscribeFromArrayTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 444f20a4d8..453916d5a6 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1243,7 +1243,14 @@ public final static Observable from(Iterable iterable) { * @see ReactiveX operators documentation: From */ public final static Observable from(T[] array) { - return from(Arrays.asList(array)); + int n = array.length; + if (n == 0) { + return empty(); + } else + if (n == 1) { + return just(array[0]); + } + return create(new OnSubscribeFromArray(array)); } /** @@ -1423,7 +1430,7 @@ public final static Observable just(final T value) { // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2) { - return from(Arrays.asList(t1, t2)); + return from((T[])new Object[] { t1, t2 }); } /** @@ -1449,7 +1456,7 @@ public final static Observable just(T t1, T t2) { // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3) { - return from(Arrays.asList(t1, t2, t3)); + return from((T[])new Object[] { t1, t2, t3 }); } /** @@ -1477,7 +1484,7 @@ public final static Observable just(T t1, T t2, T t3) { // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3, T t4) { - return from(Arrays.asList(t1, t2, t3, t4)); + return from((T[])new Object[] { t1, t2, t3, t4 }); } /** @@ -1507,7 +1514,7 @@ public final static Observable just(T t1, T t2, T t3, T t4) { // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3, T t4, T t5) { - return from(Arrays.asList(t1, t2, t3, t4, t5)); + return from((T[])new Object[] { t1, t2, t3, t4, t5 }); } /** @@ -1539,7 +1546,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5) { // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6) { - return from(Arrays.asList(t1, t2, t3, t4, t5, t6)); + return from((T[])new Object[] { t1, t2, t3, t4, t5, t6 }); } /** @@ -1573,7 +1580,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6) { // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7) { - return from(Arrays.asList(t1, t2, t3, t4, t5, t6, t7)); + return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7 }); } /** @@ -1609,7 +1616,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8) { - return from(Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8)); + return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7, t8 }); } /** @@ -1647,7 +1654,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8, T t9) { - return from(Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8, t9)); + return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7, t8, t9 }); } /** @@ -1687,7 +1694,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8, T t9, T t10) { - return from(Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10)); + return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7, t8, t9, t10 }); } /** @@ -1821,7 +1828,7 @@ public final static Observable merge(Observable Observable merge(Observable t1, Observable t2) { - return merge(from(Arrays.asList(t1, t2))); + return merge(new Observable[] { t1, t2 }); } /** @@ -1847,7 +1854,7 @@ public final static Observable merge(Observable t1, Observab */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3) { - return merge(from(Arrays.asList(t1, t2, t3))); + return merge(new Observable[] { t1, t2, t3 }); } /** @@ -1875,7 +1882,7 @@ public final static Observable merge(Observable t1, Observab */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4) { - return merge(from(Arrays.asList(t1, t2, t3, t4))); + return merge(new Observable[] { t1, t2, t3, t4 }); } /** @@ -1905,7 +1912,7 @@ public final static Observable merge(Observable t1, Observab */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { - return merge(from(Arrays.asList(t1, t2, t3, t4, t5))); + return merge(new Observable[] { t1, t2, t3, t4, t5 }); } /** @@ -1937,7 +1944,7 @@ public final static Observable merge(Observable t1, Observab */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { - return merge(from(Arrays.asList(t1, t2, t3, t4, t5, t6))); + return merge(new Observable[] { t1, t2, t3, t4, t5, t6 }); } /** @@ -1971,7 +1978,7 @@ public final static Observable merge(Observable t1, Observab */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { - return merge(from(Arrays.asList(t1, t2, t3, t4, t5, t6, t7))); + return merge(new Observable[] { t1, t2, t3, t4, t5, t6, t7 }); } /** @@ -2007,7 +2014,7 @@ public final static Observable merge(Observable t1, Observab */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { - return merge(from(Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8))); + return merge(new Observable[] { t1, t2, t3, t4, t5, t6, t7, t8 }); } /** @@ -2045,7 +2052,7 @@ public final static Observable merge(Observable t1, Observab */ @SuppressWarnings("unchecked") public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { - return merge(from(Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8, t9))); + return merge(new Observable[] { t1, t2, t3, t4, t5, t6, t7, t8, t9 }); } /** diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromArray.java b/src/main/java/rx/internal/operators/OnSubscribeFromArray.java new file mode 100644 index 0000000000..623dcaa65f --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribeFromArray.java @@ -0,0 +1,128 @@ +/** + * 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.atomic.AtomicLong; + +import rx.*; +import rx.Observable.OnSubscribe; + +public final class OnSubscribeFromArray implements OnSubscribe { + final T[] array; + public OnSubscribeFromArray(T[] array) { + this.array = array; + } + + @Override + public void call(Subscriber child) { + child.setProducer(new FromArrayProducer(child, array)); + } + + static final class FromArrayProducer + extends AtomicLong + implements Producer { + /** */ + private static final long serialVersionUID = 3534218984725836979L; + + final Subscriber child; + final T[] array; + + int index; + + public FromArrayProducer(Subscriber child, T[] array) { + this.child = child; + this.array = array; + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } + if (n == Long.MAX_VALUE) { + if (BackpressureUtils.getAndAddRequest(this, n) == 0) { + fastPath(); + } + } else + if (n != 0) { + if (BackpressureUtils.getAndAddRequest(this, n) == 0) { + slowPath(n); + } + } + } + + void fastPath() { + final Subscriber child = this.child; + + for (T t : array) { + if (child.isUnsubscribed()) { + return; + } + + child.onNext(t); + } + + if (child.isUnsubscribed()) { + return; + } + child.onCompleted(); + } + + void slowPath(long r) { + final Subscriber child = this.child; + final T[] array = this.array; + final int n = array.length; + + long e = 0L; + int i = index; + + for (;;) { + + while (r != 0L && i != n) { + if (child.isUnsubscribed()) { + return; + } + + child.onNext(array[i]); + + i++; + + if (i == n) { + if (!child.isUnsubscribed()) { + child.onCompleted(); + } + return; + } + + r--; + e--; + } + + r = get() + e; + + if (r == 0L) { + index = i; + r = addAndGet(e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + } +} diff --git a/src/perf/java/rx/operators/FromComparison.java b/src/perf/java/rx/operators/FromComparison.java new file mode 100644 index 0000000000..7a7a12545d --- /dev/null +++ b/src/perf/java/rx/operators/FromComparison.java @@ -0,0 +1,123 @@ +/** + * 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.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import rx.*; +import rx.internal.operators.*; + +/** + * Benchmark typical atomic operations on volatile fields and AtomicXYZ classes. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*FromComparison.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class FromComparison { + @Param({ "1", "10", "100", "1000", "1000000" }) + public int times; + + Observable iterableSource; + + Observable arraySource; + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + + Arrays.fill(array, 1); + + iterableSource = Observable.create(new OnSubscribeFromIterable(Arrays.asList(array))); + arraySource = Observable.create(new OnSubscribeFromArray(array)); + } + + @Benchmark + public void fastpathIterable(Blackhole bh) { + iterableSource.subscribe(new RequestingSubscriber(bh, Long.MAX_VALUE)); + } + + @Benchmark + public void fastpathArray(Blackhole bh) { + arraySource.subscribe(new RequestingSubscriber(bh, Long.MAX_VALUE)); + } + + @Benchmark + public void slowpathIterable(Blackhole bh) { + iterableSource.subscribe(new RequestingSubscriber(bh, times + 1)); + } + + @Benchmark + public void slowpathArray(Blackhole bh) { + arraySource.subscribe(new RequestingSubscriber(bh, times + 1)); + } + + @Benchmark + public void slowpathIterable2(Blackhole bh) { + iterableSource.subscribe(new RequestingSubscriber(bh, 128)); + } + + @Benchmark + public void slowpathArray2(Blackhole bh) { + arraySource.subscribe(new RequestingSubscriber(bh, 128)); + } + + + static final class RequestingSubscriber extends Subscriber { + final Blackhole bh; + final long limit; + long received; + Producer p; + + public RequestingSubscriber(Blackhole bh, long limit) { + this.bh = bh; + this.limit = limit; + } + + @Override + public void onNext(T t) { + bh.consume(t); + if (++received >= limit) { + received = 0L; + p.request(limit); + } + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onCompleted() { + + } + + @Override + public void setProducer(Producer p) { + this.p = p; + p.request(limit); + } + } +} diff --git a/src/test/java/rx/internal/operators/OnSubscribeFromArrayTest.java b/src/test/java/rx/internal/operators/OnSubscribeFromArrayTest.java new file mode 100644 index 0000000000..3b7ec5220b --- /dev/null +++ b/src/test/java/rx/internal/operators/OnSubscribeFromArrayTest.java @@ -0,0 +1,67 @@ +/** + * 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 org.junit.Test; + +import rx.Observable; +import rx.observers.TestSubscriber; + +public class OnSubscribeFromArrayTest { + + Observable create(int n) { + Integer[] array = new Integer[n]; + for (int i = 0; i < n; i++) { + array[i] = i; + } + return Observable.create(new OnSubscribeFromArray(array)); + } + @Test + public void simple() { + TestSubscriber ts = new TestSubscriber(); + + create(1000).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(1000); + ts.assertCompleted(); + } + + @Test + public void backpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + create(1000).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotCompleted(); + + ts.requestMore(10); + + ts.assertNoErrors(); + ts.assertValueCount(10); + ts.assertNotCompleted(); + + ts.requestMore(1000); + + ts.assertNoErrors(); + ts.assertValueCount(1000); + ts.assertCompleted(); + } + +} From 1d643e1775512327d9eba242309623abdbb1e542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 28 Oct 2015 22:48:17 +0100 Subject: [PATCH 426/857] 1.x: perf benchmark for the cost of subscribing. --- src/perf/java/rx/SubscribingPerf.java | 182 ++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/perf/java/rx/SubscribingPerf.java diff --git a/src/perf/java/rx/SubscribingPerf.java b/src/perf/java/rx/SubscribingPerf.java new file mode 100644 index 0000000000..cdc229c8b9 --- /dev/null +++ b/src/perf/java/rx/SubscribingPerf.java @@ -0,0 +1,182 @@ +/** + * 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; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmark the cost of subscription and initial request management. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*SubscribingPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class SubscribingPerf { + + Observable just = Observable.just(1); + Observable range = Observable.range(1, 2); + + @Benchmark + public void justDirect(Blackhole bh) { + just.subscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void justStarted(Blackhole bh) { + just.subscribe(new StartedSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void justUsual(Blackhole bh) { + just.subscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void rangeDirect(Blackhole bh) { + range.subscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void rangeStarted(Blackhole bh) { + range.subscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void rangeUsual(Blackhole bh) { + range.subscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void justDirectUnsafe(Blackhole bh) { + just.unsafeSubscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void justStartedUnsafe(Blackhole bh) { + just.unsafeSubscribe(new StartedSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void justUsualUnsafe(Blackhole bh) { + just.unsafeSubscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void rangeDirectUnsafe(Blackhole bh) { + range.unsafeSubscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void rangeStartedUnsafe(Blackhole bh) { + range.unsafeSubscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + } + + @Benchmark + public void rangeUsualUnsafe(Blackhole bh) { + range.unsafeSubscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + } + + + static final class DirectSubscriber extends Subscriber { + final long r; + final Blackhole bh; + public DirectSubscriber(long r, Blackhole bh) { + this.r = r; + this.bh = bh; + } + @Override + public void onNext(T t) { + bh.consume(t); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onCompleted() { + } + + @Override + public void setProducer(Producer p) { + p.request(r); + } + } + + static final class StartedSubscriber extends Subscriber { + final long r; + final Blackhole bh; + public StartedSubscriber(long r, Blackhole bh) { + this.r = r; + this.bh = bh; + } + + @Override + public void onStart() { + request(r); + } + + @Override + public void onNext(T t) { + bh.consume(t); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onCompleted() { + + } + } + + /** + * This requests in the constructor. + * @param the value type + */ + static final class UsualSubscriber extends Subscriber { + final Blackhole bh; + public UsualSubscriber(long r, Blackhole bh) { + this.bh = bh; + request(r); + } + + @Override + public void onNext(T t) { + bh.consume(t); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onCompleted() { + + } + } +} From 6c19a7b6e37326a94b9a5e8fcc716fe67160724e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 29 Oct 2015 09:16:13 +0100 Subject: [PATCH 427/857] 1.x: update and bugfix to SubscribingPerf Two of the tests used the wrong subscriber. Added a benchmark which should help verify the overhead of checking isUnsubscribed within range in #3479 because I suspect that will get worse there. --- src/perf/java/rx/SubscribingPerf.java | 83 +++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/src/perf/java/rx/SubscribingPerf.java b/src/perf/java/rx/SubscribingPerf.java index cdc229c8b9..f172f00852 100644 --- a/src/perf/java/rx/SubscribingPerf.java +++ b/src/perf/java/rx/SubscribingPerf.java @@ -21,6 +21,8 @@ import org.openjdk.jmh.annotations.*; import org.openjdk.jmh.infra.Blackhole; +import rx.functions.Func1; + /** * Benchmark the cost of subscription and initial request management. *

@@ -38,64 +40,121 @@ public class SubscribingPerf { @Benchmark public void justDirect(Blackhole bh) { - just.subscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + DirectSubscriber subscriber = new DirectSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + just.subscribe(subscriber); } @Benchmark public void justStarted(Blackhole bh) { - just.subscribe(new StartedSubscriber(Long.MAX_VALUE, bh)); + StartedSubscriber subscriber = new StartedSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + just.subscribe(subscriber); } @Benchmark public void justUsual(Blackhole bh) { - just.subscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + UsualSubscriber subscriber = new UsualSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + just.subscribe(subscriber); } @Benchmark public void rangeDirect(Blackhole bh) { - range.subscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + DirectSubscriber subscriber = new DirectSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + range.subscribe(subscriber); } @Benchmark public void rangeStarted(Blackhole bh) { - range.subscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + StartedSubscriber subscriber = new StartedSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + range.subscribe(subscriber); } @Benchmark public void rangeUsual(Blackhole bh) { - range.subscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + UsualSubscriber subscriber = new UsualSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + range.subscribe(subscriber); } @Benchmark public void justDirectUnsafe(Blackhole bh) { - just.unsafeSubscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + DirectSubscriber subscriber = new DirectSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + just.unsafeSubscribe(subscriber); } @Benchmark public void justStartedUnsafe(Blackhole bh) { - just.unsafeSubscribe(new StartedSubscriber(Long.MAX_VALUE, bh)); + StartedSubscriber subscriber = new StartedSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + just.unsafeSubscribe(subscriber); } @Benchmark public void justUsualUnsafe(Blackhole bh) { - just.unsafeSubscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + UsualSubscriber subscriber = new UsualSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + just.unsafeSubscribe(subscriber); } @Benchmark public void rangeDirectUnsafe(Blackhole bh) { - range.unsafeSubscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + DirectSubscriber subscriber = new DirectSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + range.unsafeSubscribe(subscriber); } @Benchmark public void rangeStartedUnsafe(Blackhole bh) { - range.unsafeSubscribe(new DirectSubscriber(Long.MAX_VALUE, bh)); + StartedSubscriber subscriber = new StartedSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + range.unsafeSubscribe(subscriber); } @Benchmark public void rangeUsualUnsafe(Blackhole bh) { - range.unsafeSubscribe(new UsualSubscriber(Long.MAX_VALUE, bh)); + UsualSubscriber subscriber = new UsualSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + range.unsafeSubscribe(subscriber); } + @State(Scope.Thread) + public static class Chain { + @Param({"10", "1000", "1000000"}) + public int times; + + @Param({"1", "2", "3", "4", "5"}) + public int maps; + + Observable source; + + @Setup + public void setup() { + Observable o = Observable.range(1, times); + + for (int i = 0; i < maps; i++) { + o = o.map(new Func1() { + @Override + public Integer call(Integer v) { + return v + 1; + } + }); + } + + source = o; + } + + @Benchmark + public void mapped(Chain c, Blackhole bh) { + DirectSubscriber subscriber = new DirectSubscriber(Long.MAX_VALUE, bh); + bh.consume(subscriber); + c.source.subscribe(subscriber); + } + } static final class DirectSubscriber extends Subscriber { final long r; From fdef36625dfb3f81520c2c7e0ebcd60f8dbe92b4 Mon Sep 17 00:00:00 2001 From: George Campbell Date: Fri, 25 Sep 2015 12:00:50 -0700 Subject: [PATCH 428/857] Begin the steps to release 1.0.15 --- CHANGES.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 5a443e1a37..444b8f33e2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,43 @@ # RxJava Releases # +### Version 1.0.15 – October 9 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.15%7C)) ### + +* [Pull 3438] (https://github.com/ReactiveX/RxJava/pull/3438) Better null tolerance in rx.exceptions.*Exception classes +* [Pull 3455] (https://github.com/ReactiveX/RxJava/pull/3455) OnErrorFailedException fix +* [Pull 3448] (https://github.com/ReactiveX/RxJava/pull/3448) Single delay +* [Pull 3429] (https://github.com/ReactiveX/RxJava/pull/3429) Removed the alias BlockingObservable#run +* [Pull 3417] (https://github.com/ReactiveX/RxJava/pull/3417) Add Single.doOnSuccess() +* [Pull 3418] (https://github.com/ReactiveX/RxJava/pull/3418) Add Single.fromCallable() +* [Pull 3419] (https://github.com/ReactiveX/RxJava/pull/3419) Add Single.doOnError() +* [Pull 3423] (https://github.com/ReactiveX/RxJava/pull/3423) Renaming Observable#x to Observable#extend +* [Pull 3174] (https://github.com/ReactiveX/RxJava/pull/3174) Blocking subscribe methods for convenience +* [Pull 3351] (https://github.com/ReactiveX/RxJava/pull/3351) Make BlockingOperatorToIterator exert backpressure. +* [Pull 3357] (https://github.com/ReactiveX/RxJava/pull/3357) Eager ConcatMap +* [Pull 3342] (https://github.com/ReactiveX/RxJava/pull/3342) Remove redundant onStart implementation in OperatorGroupBy +* [Pull 3361] (https://github.com/ReactiveX/RxJava/pull/3361) Safer error handling in BlockingOperatorToFuture +* [Pull 3363] (https://github.com/ReactiveX/RxJava/pull/3363) Remove unused private method from CachedObservable and make "state" final +* [Pull 3408] (https://github.com/ReactiveX/RxJava/pull/3408) DoOnEach: report both original exception and callback exception. +* [Pull 3386] (https://github.com/ReactiveX/RxJava/pull/3386) Changed javadoc for Observable.doOnRequest(Action1) +* [Pull 3149] (https://github.com/ReactiveX/RxJava/pull/3149) Scheduler shutdown capability +* [Pull 3384] (https://github.com/ReactiveX/RxJava/pull/3384) Fix for take() reentrancy bug. +* [Pull 3356] (https://github.com/ReactiveX/RxJava/pull/3356) Fix to a bunch of bugs and issues with AsyncOnSubscribe +* [Pull 3362] (https://github.com/ReactiveX/RxJava/pull/3362) Fix synchronization on non-final field in BufferUntilSubscriber +* [Pull 3365] (https://github.com/ReactiveX/RxJava/pull/3365) Make field final and remove unnecessary unboxing in OnSubscribeRedo.RetryWithPredicate +* [Pull 3370] (https://github.com/ReactiveX/RxJava/pull/3370) Remove unused field updater from SubjectSubscriptionManager +* [Pull 3369] (https://github.com/ReactiveX/RxJava/pull/3369) Lint fixes for unnecessary unboxing +* [Pull 3203] (https://github.com/ReactiveX/RxJava/pull/3203) Implemented the AsyncOnSubscribe +* [Pull 3340] (https://github.com/ReactiveX/RxJava/pull/3340) test/subjects: Use statically imported never() methods +* [Pull 3154] (https://github.com/ReactiveX/RxJava/pull/3154) Add Observable.fromCallable() as a companion for Observable.defer() +* [Pull 3285] (https://github.com/ReactiveX/RxJava/pull/3285) Added latch to async SyncOnSubscrbeTest +* [Pull 3118] (https://github.com/ReactiveX/RxJava/pull/3118) Implementing the SyncOnSubscribe +* [Pull 3183] (https://github.com/ReactiveX/RxJava/pull/3183) Refactored exception reporting of most operators. +* [Pull 3214] (https://github.com/ReactiveX/RxJava/pull/3214) Fix to Notification equals method. +* [Pull 3171] (https://github.com/ReactiveX/RxJava/pull/3171) Scan backpressure and first emission fix +* [Pull 3181] (https://github.com/ReactiveX/RxJava/pull/3181) MapNotification producer NPE fix +* [Pull 3167] (https://github.com/ReactiveX/RxJava/pull/3167) Fixed negative request due to unsubscription of a large requester +* [Pull 3177] (https://github.com/ReactiveX/RxJava/pull/3177) BackpressureUtils capped add/multiply methods + tests +* [Pull 3155] (https://github.com/ReactiveX/RxJava/pull/3155) SafeSubscriber - report onCompleted unsubscribe error to RxJavaPlugin + ### Version 1.0.14 – August 12th 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.14%7C)) ### * [Pull 2963] (https://github.com/ReactiveX/RxJava/pull/2963) Set of standard producers and updated queue implementations From 2e1b5904c7ebebe768924a7e2ad2060ef6faf6ed Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 2 Nov 2015 16:41:07 +0100 Subject: [PATCH 429/857] 1.x: fix scan() not accepting a null initial value I forgot a NotificationLite conversion in the constructor. Note that there were no tests verifying null behavior at all. --- .../rx/internal/operators/OperatorScan.java | 5 ++- .../internal/operators/OperatorScanTest.java | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index 1cbdb53d54..5b132fd767 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -36,6 +36,9 @@ *

* Note that when you pass a seed to {@code scan} the resulting Observable will emit that seed as its * first emitted item. + * + * @param the aggregate and output type + * @param the input value type */ public final class OperatorScan implements Operator { @@ -192,7 +195,7 @@ public InitialProducer(R initialValue, Subscriber child) { q = new SpscLinkedAtomicQueue(); // new SpscUnboundedAtomicArrayQueue(8); } this.queue = q; - q.offer(initialValue); + q.offer(NotificationLite.instance().next(initialValue)); } @Override diff --git a/src/test/java/rx/internal/operators/OperatorScanTest.java b/src/test/java/rx/internal/operators/OperatorScanTest.java index ac7772753f..96c1b1dbe1 100644 --- a/src/test/java/rx/internal/operators/OperatorScanTest.java +++ b/src/test/java/rx/internal/operators/OperatorScanTest.java @@ -391,4 +391,39 @@ public Integer call(Integer t1, Integer t2) { ts.assertNotCompleted(); ts.assertValue(0); } + + @Test + public void testInitialValueNull() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(1, 10).scan(null, new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + if (t1 == null) { + return t2; + } + return t1 + t2; + } + }).subscribe(ts); + + ts.assertValues(null, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testEverythingIsNull() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(1, 6).scan(null, new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return null; + } + }).subscribe(ts); + + ts.assertValues(null, null, null, null, null, null, null); + ts.assertNoErrors(); + ts.assertCompleted(); + } } From 93908564fd20eb990777508c7273ca83ce5b3616 Mon Sep 17 00:00:00 2001 From: shoma2da Date: Tue, 3 Nov 2015 01:55:56 +0900 Subject: [PATCH 430/857] Remove unused imports --- src/main/java/rx/Single.java | 1 - src/main/java/rx/internal/operators/OperatorDelay.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index e082daeaab..64447185a8 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -21,7 +21,6 @@ import rx.annotations.Experimental; import rx.exceptions.Exceptions; import rx.exceptions.OnErrorNotImplementedException; -import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; diff --git a/src/main/java/rx/internal/operators/OperatorDelay.java b/src/main/java/rx/internal/operators/OperatorDelay.java index 4c0172f692..7edf5199b3 100644 --- a/src/main/java/rx/internal/operators/OperatorDelay.java +++ b/src/main/java/rx/internal/operators/OperatorDelay.java @@ -17,7 +17,6 @@ import java.util.concurrent.TimeUnit; -import rx.Observable; import rx.Observable.Operator; import rx.Scheduler; import rx.Scheduler.Worker; From 91d3d3a67a678ce98d46f90789dca3911c531bfb Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Nov 2015 10:05:06 +0100 Subject: [PATCH 431/857] 1.x: make scan's delayed Producer independent of event serialization It turns out serializing `request()` calls with regular `onXXX()` calls can be problematic because a `request()` may trigger an emission of events which then end up being queued (since `emitting == true`). If the request is large and the queue otherwise unbounded, this will likely cause OOME. In case of `scan`, the fix was to make the missing request accounting and arrival of the `Producer` independent of the event's emitter loop; there is no need for them to be serialized in respect to each other. In case of the `ProducerObserverArbiter` where the request accounting and producer swapping has to be serialized with the value emission, the solution is to call `request()` outside the emitter-loop. --- .../rx/internal/operators/OperatorScan.java | 141 +++++++----------- .../producers/ProducerObserverArbiter.java | 50 ++++--- .../internal/operators/OperatorScanTest.java | 20 +++ .../rx/internal/producers/ProducersTest.java | 50 ++++++- 4 files changed, 156 insertions(+), 105 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index 5b132fd767..f91d9b28f2 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -16,6 +16,7 @@ package rx.internal.operators; import java.util.Queue; +import java.util.concurrent.atomic.AtomicLong; import rx.*; import rx.Observable.Operator; @@ -175,12 +176,10 @@ static final class InitialProducer implements Producer, Observer { boolean missed; /** Missed a request. */ long missedRequested; - /** Missed a producer. */ - Producer missedProducer; /** The current requested amount. */ - long requested; + final AtomicLong requested; /** The current producer. */ - Producer producer; + volatile Producer producer; volatile boolean done; Throwable error; @@ -196,41 +195,7 @@ public InitialProducer(R initialValue, Subscriber child) { } this.queue = q; q.offer(NotificationLite.instance().next(initialValue)); - } - - @Override - public void request(long n) { - if (n < 0L) { - throw new IllegalArgumentException("n >= required but it was " + n); - } else - if (n != 0L) { - synchronized (this) { - if (emitting) { - long mr = missedRequested; - long mu = mr + n; - if (mu < 0L) { - mu = Long.MAX_VALUE; - } - missedRequested = mu; - return; - } - emitting = true; - } - - long r = requested; - long u = r + n; - if (u < 0L) { - u = Long.MAX_VALUE; - } - requested = u; - - Producer p = producer; - if (p != null) { - p.request(n); - } - - emitLoop(); - } + this.requested = new AtomicLong(); } @Override @@ -270,23 +235,51 @@ public void onCompleted() { emit(); } + @Override + public void request(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= required but it was " + n); + } else + if (n != 0L) { + BackpressureUtils.getAndAddRequest(requested, n); + Producer p = producer; + if (p == null) { + // not synchronizing on this to avoid clash with emit() + synchronized (requested) { + p = producer; + if (p == null) { + long mr = missedRequested; + missedRequested = BackpressureUtils.addCap(mr, n); + } + } + } + if (p != null) { + p.request(n); + } + emit(); + } + } + public void setProducer(Producer p) { if (p == null) { throw new NullPointerException(); } - synchronized (this) { - if (emitting) { - missedProducer = p; - return; + long mr; + // not synchronizing on this to avoid clash with emit() + synchronized (requested) { + if (producer != null) { + throw new IllegalStateException("Can't set more than one Producer!"); } - emitting = true; + // request one less because of the initial value, this happens once + mr = missedRequested - 1; + missedRequested = 0L; + producer = p; } - producer = p; - long r = requested; - if (r != 0L) { - p.request(r); + + if (mr > 0L) { + p.request(mr); } - emitLoop(); + emit(); } void emit() { @@ -304,7 +297,9 @@ void emitLoop() { final Subscriber child = this.child; final Queue queue = this.queue; final NotificationLite nl = NotificationLite.instance(); - long r = requested; + AtomicLong requested = this.requested; + + long r = requested.get(); for (;;) { boolean max = r == Long.MAX_VALUE; boolean d = done; @@ -312,6 +307,7 @@ void emitLoop() { if (checkTerminated(d, empty, child)) { return; } + long e = 0L; while (r != 0L) { d = done; Object o = queue.poll(); @@ -325,52 +321,25 @@ void emitLoop() { R v = nl.getValue(o); try { child.onNext(v); - } catch (Throwable e) { - Exceptions.throwIfFatal(e); - child.onError(OnErrorThrowable.addValueAsLastCause(e, v)); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + child.onError(OnErrorThrowable.addValueAsLastCause(ex, v)); return; } - if (!max) { - r--; - } + r--; + e--; } - if (!max) { - requested = r; + + if (e != 0 && !max) { + r = requested.addAndGet(e); } - Producer p; - long mr; synchronized (this) { - p = missedProducer; - mr = missedRequested; - if (!missed && p == null && mr == 0L) { + if (!missed) { emitting = false; return; } missed = false; - missedProducer = null; - missedRequested = 0L; - } - - if (mr != 0L && !max) { - long u = r + mr; - if (u < 0L) { - u = Long.MAX_VALUE; - } - requested = u; - r = u; - } - - if (p != null) { - producer = p; - if (r != 0L) { - p.request(r); - } - } else { - p = producer; - if (p != null && mr != 0L) { - p.request(mr); - } } } } diff --git a/src/main/java/rx/internal/producers/ProducerObserverArbiter.java b/src/main/java/rx/internal/producers/ProducerObserverArbiter.java index 7600815094..985352a3f4 100644 --- a/src/main/java/rx/internal/producers/ProducerObserverArbiter.java +++ b/src/main/java/rx/internal/producers/ProducerObserverArbiter.java @@ -20,6 +20,7 @@ import rx.*; import rx.Observer; import rx.exceptions.*; +import rx.internal.operators.BackpressureUtils; /** * Producer that serializes any event emission with requesting and producer changes. @@ -135,6 +136,7 @@ public void request(long n) { } emitting = true; } + Producer p = currentProducer; boolean skipFinal = false; try { long r = requested; @@ -143,12 +145,7 @@ public void request(long n) { u = Long.MAX_VALUE; } requested = u; - - Producer p = currentProducer; - if (p != null) { - p.request(n); - } - + emitLoop(); skipFinal = true; } finally { @@ -158,6 +155,9 @@ public void request(long n) { } } } + if (p != null) { + p.request(n); + } } public void setProducer(Producer p) { @@ -169,12 +169,9 @@ public void setProducer(Producer p) { emitting = true; } boolean skipFinal = false; + currentProducer = p; + long r = requested; try { - currentProducer = p; - long r = requested; - if (p != null && r != 0) { - p.request(r); - } emitLoop(); skipFinal = true; } finally { @@ -184,17 +181,24 @@ public void setProducer(Producer p) { } } } + if (p != null && r != 0) { + p.request(r); + } } void emitLoop() { final Subscriber c = child; + long toRequest = 0L; + Producer requestFrom = null; + outer: for (;;) { long localRequested; Producer localProducer; Object localTerminal; List q; + boolean quit = false; synchronized (this) { localRequested = missedRequested; localProducer = missedProducer; @@ -203,13 +207,21 @@ void emitLoop() { if (localRequested == 0L && localProducer == null && q == null && localTerminal == null) { emitting = false; - return; + quit = true; + } else { + missedRequested = 0L; + missedProducer = null; + queue = null; + missedTerminal = null; } - missedRequested = 0L; - missedProducer = null; - queue = null; - missedTerminal = null; } + if (quit) { + if (toRequest != 0L && requestFrom != null) { + requestFrom.request(toRequest); + } + return; + } + boolean empty = q == null || q.isEmpty(); if (localTerminal != null) { if (localTerminal != Boolean.TRUE) { @@ -266,13 +278,15 @@ void emitLoop() { } else { currentProducer = localProducer; if (r != 0L) { - localProducer.request(r); + toRequest = BackpressureUtils.addCap(toRequest, r); + requestFrom = localProducer; } } } else { Producer p = currentProducer; if (p != null && localRequested != 0L) { - p.request(localRequested); + toRequest = BackpressureUtils.addCap(toRequest, localRequested); + requestFrom = p; } } } diff --git a/src/test/java/rx/internal/operators/OperatorScanTest.java b/src/test/java/rx/internal/operators/OperatorScanTest.java index 96c1b1dbe1..d053694dd9 100644 --- a/src/test/java/rx/internal/operators/OperatorScanTest.java +++ b/src/test/java/rx/internal/operators/OperatorScanTest.java @@ -426,4 +426,24 @@ public Integer call(Integer t1, Integer t2) { ts.assertNoErrors(); ts.assertCompleted(); } + + @Test(timeout = 1000) + public void testUnboundedSource() { + Observable.range(0, Integer.MAX_VALUE) + .scan(0, new Func2() { + @Override + public Integer call(Integer a, Integer b) { + return 0; + } + }) + .subscribe(new TestSubscriber() { + int count; + @Override + public void onNext(Integer t) { + if (++count == 2) { + unsubscribe(); + } + } + }); + } } diff --git a/src/test/java/rx/internal/producers/ProducersTest.java b/src/test/java/rx/internal/producers/ProducersTest.java index 0e5beacdfa..81377f29a3 100644 --- a/src/test/java/rx/internal/producers/ProducersTest.java +++ b/src/test/java/rx/internal/producers/ProducersTest.java @@ -23,8 +23,8 @@ import org.junit.*; import rx.*; -import rx.Observable.OnSubscribe; import rx.Observable; +import rx.Observable.*; import rx.Observer; import rx.functions.*; import rx.observers.TestSubscriber; @@ -378,4 +378,52 @@ public void testObserverArbiterAsync() { 20L, 21L, 22L, 23L, 24L, 40L, 41L, 42L, 43L, 44L)); } + + @Test(timeout = 1000) + public void testProducerObserverArbiterUnbounded() { + Observable.range(0, Integer.MAX_VALUE) + .lift(new Operator() { + @Override + public Subscriber call(Subscriber t) { + final ProducerObserverArbiter poa = new ProducerObserverArbiter(t); + + Subscriber parent = new Subscriber() { + + @Override + public void onCompleted() { + poa.onCompleted(); + } + + @Override + public void onError(Throwable e) { + poa.onError(e); + } + + @Override + public void onNext(Integer t) { + poa.onNext(t); + } + + + @Override + public void setProducer(Producer p) { + poa.setProducer(p); + } + }; + + t.add(parent); + t.setProducer(poa); + + return parent; + } + }).subscribe(new TestSubscriber() { + int count; + @Override + public void onNext(Integer t) { + if (++count == 2) { + unsubscribe(); + } + } + }); + } } From 7f3173b871e78eb62cdfd8d0e6d15eb71ef326ba Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 4 Nov 2015 15:15:21 +0100 Subject: [PATCH 432/857] 1.x: fix for zip(Obs>) backpressure problem Reported in #3492. --- .../rx/internal/operators/OperatorZip.java | 6 ++-- .../internal/operators/OperatorZipTest.java | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorZip.java b/src/main/java/rx/internal/operators/OperatorZip.java index d4f0560718..8e2f1c1e4d 100644 --- a/src/main/java/rx/internal/operators/OperatorZip.java +++ b/src/main/java/rx/internal/operators/OperatorZip.java @@ -111,8 +111,11 @@ public OperatorZip(Func9 f) { public Subscriber call(final Subscriber child) { final Zip zipper = new Zip(child, zipFunction); final ZipProducer producer = new ZipProducer(zipper); - child.setProducer(producer); final ZipSubscriber subscriber = new ZipSubscriber(child, zipper, producer); + + child.add(subscriber); + child.setProducer(producer); + return subscriber; } @@ -124,7 +127,6 @@ private final class ZipSubscriber extends Subscriber { final ZipProducer producer; public ZipSubscriber(Subscriber child, Zip zipper, ZipProducer producer) { - super(child); this.child = child; this.zipper = zipper; this.producer = producer; diff --git a/src/test/java/rx/internal/operators/OperatorZipTest.java b/src/test/java/rx/internal/operators/OperatorZipTest.java index d9487c5b03..23103448f8 100644 --- a/src/test/java/rx/internal/operators/OperatorZipTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipTest.java @@ -1313,4 +1313,32 @@ public Integer call(Integer t1, Integer t2) { ts.assertNoErrors(); ts.assertReceivedOnNext(Arrays.asList(11)); } + + @SuppressWarnings("cast") + @Test + public void testZipObservableObservableBackpressure() { + @SuppressWarnings("unchecked") + Observable[] osArray = new Observable[] { + Observable.range(0, 10), + Observable.range(0, 10) + }; + + Observable> os = (Observable>) Observable.from(osArray); + Observable o1 = Observable.zip(os, new FuncN() { + @Override + public Integer call(Object... a) { + return 0; + } + }); + + TestSubscriber sub1 = TestSubscriber.create(5); + + o1.subscribe(sub1); + + sub1.requestMore(5); + + sub1.assertValueCount(10); + sub1.assertNoErrors(); + sub1.assertCompleted(); + } } From 88ea0923a4edde88e98787ec9825ef868819e064 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 5 Nov 2015 09:30:25 +0100 Subject: [PATCH 433/857] 1.x: benchmark just() and its optimizations. --- src/perf/java/rx/ScalarJustPerf.java | 197 +++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 src/perf/java/rx/ScalarJustPerf.java diff --git a/src/perf/java/rx/ScalarJustPerf.java b/src/perf/java/rx/ScalarJustPerf.java new file mode 100644 index 0000000000..24543852ff --- /dev/null +++ b/src/perf/java/rx/ScalarJustPerf.java @@ -0,0 +1,197 @@ +/** + * 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; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import rx.functions.Func1; +import rx.jmh.LatchedObserver; +import rx.schedulers.Schedulers; + +/** + * Benchmark the cost of just and its various optimizations. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*ScalarJustPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class ScalarJustPerf { + /** A subscriber without a CountDownLatch; use it for synchronous testing only. */ + static final class PlainSubscriber extends Subscriber { + final Blackhole bh; + public PlainSubscriber(Blackhole bh) { + this.bh = bh; + } + + @Override + public void onNext(Integer t) { + bh.consume(t); + } + + @Override + public void onError(Throwable e) { + bh.consume(e); + } + + @Override + public void onCompleted() { + bh.consume(false); + } + } + + /** This is a simple just. */ + Observable simple; + /** + * This is a simple just observed on the computation scheduler. + * The current computation scheduler supports direct scheduling and should have + * lower overhead than a regular createWorker-use-unsubscribe. + */ + Observable observeOn; + /** This is a simple just observed on the IO thread. */ + Observable observeOnIO; + + /** + * This is a simple just subscribed to on the computation scheduler. + * In theory, for non-backpressured just(), this should be the + * same as observeOn. + */ + Observable subscribeOn; + /** This is a simple just subscribed to on the IO scheduler. */ + Observable subscribeOnIO; + + /** This is a just mapped to itself which should skip the operator flatMap completely. */ + Observable justFlatMapJust; + /** + * This is a just mapped to a range of 2 elements; it tests the case where the inner + * Observable isn't a just(). + */ + Observable justFlatMapRange; + + @Setup + public void setup() { + simple = Observable.just(1); + + observeOn = simple.observeOn(Schedulers.computation()); + observeOnIO = simple.observeOn(Schedulers.io()); + + subscribeOn = simple.subscribeOn(Schedulers.computation()); + subscribeOnIO = simple.subscribeOn(Schedulers.io()); + + justFlatMapJust = simple.flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.just(v); + } + }); + + justFlatMapRange = simple.flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }); + } + + /** + * Common routine to create a latched observer, subscribe it to the + * given source and spin-wait for its completion. + *

Don't use this with long sources. The spin-wait is there + * to avoid operating-system level scheduling-wakeup granularity problems with + * short sources. + * @param bh the black hole to sink values and prevent dead code elimination + * @param source the source observable to observe + */ + void runAsync(Blackhole bh, Observable source) { + LatchedObserver lo = new LatchedObserver(bh); + + source.subscribe(lo); + + while (lo.latch.getCount() != 0L); + } + + @Benchmark + public void simple(Blackhole bh) { + PlainSubscriber s = new PlainSubscriber(bh); + simple.subscribe(s); + } + + @Benchmark + public void simpleEscape(Blackhole bh) { + PlainSubscriber s = new PlainSubscriber(bh); + bh.consume(s); + simple.subscribe(s); + } + + @Benchmark + public Object simpleEscapeAll(Blackhole bh) { + PlainSubscriber s = new PlainSubscriber(bh); + bh.consume(s); + return simple.subscribe(s); + } + + @Benchmark + public void observeOn(Blackhole bh) { + runAsync(bh, observeOn); + } + + @Benchmark + public void observeOnIO(Blackhole bh) { + runAsync(bh, observeOnIO); + } + + @Benchmark + public void subscribeOn(Blackhole bh) { + runAsync(bh, subscribeOn); + } + + @Benchmark + public void subscribeOnIO(Blackhole bh) { + runAsync(bh, subscribeOnIO); + } + + @Benchmark + public void justFlatMapJust(Blackhole bh) { + PlainSubscriber s = new PlainSubscriber(bh); + justFlatMapJust.subscribe(s); + } + + @Benchmark + public void justFlatMapJustEscape(Blackhole bh) { + PlainSubscriber s = new PlainSubscriber(bh); + bh.consume(s); + justFlatMapJust.subscribe(s); + } + + @Benchmark + public void justFlatMapRange(Blackhole bh) { + PlainSubscriber s = new PlainSubscriber(bh); + justFlatMapRange.subscribe(s); + } + + @Benchmark + public void justFlatMapRangeEscape(Blackhole bh) { + PlainSubscriber s = new PlainSubscriber(bh); + bh.consume(s); + justFlatMapRange.subscribe(s); + } +} From 5427db8892fe67e2c3e2148c46dccaeaff0f3c3a Mon Sep 17 00:00:00 2001 From: Tomasz Drodowski Date: Sat, 7 Nov 2015 18:02:36 +0100 Subject: [PATCH 434/857] Some code clean ups. Nothing that could change logic or application flow, just minor refactors to be consistent with good practices and clean code. --- .../internal/operators/BlockingOperatorNext.java | 4 ++-- .../rx/internal/operators/OnSubscribeRedo.java | 2 +- .../operators/OperatorBufferWithSize.java | 1 - .../rx/internal/operators/OperatorConcat.java | 2 +- .../operators/OperatorWindowWithSize.java | 1 - .../operators/OperatorWithLatestFrom.java | 1 - .../internal/schedulers/EventLoopsScheduler.java | 10 ++++------ .../GenericScheduledExecutorService.java | 1 - .../rx/internal/util/SubscriptionRandomList.java | 2 +- .../java/rx/observables/AbstractOnSubscribe.java | 5 +---- .../java/rx/observables/AsyncOnSubscribe.java | 2 +- src/main/java/rx/subjects/ReplaySubject.java | 2 +- .../rx/subscriptions/RefCountSubscription.java | 2 +- src/test/java/rx/CovarianceTest.java | 2 +- .../operators/OnSubscribeCombineLatestTest.java | 9 +++------ .../internal/operators/OnSubscribeUsingTest.java | 6 +++--- .../rx/internal/operators/OperatorConcatTest.java | 2 +- .../internal/operators/OperatorGroupByTest.java | 4 ++-- .../rx/internal/operators/OperatorMapTest.java | 2 +- .../rx/internal/operators/OperatorMergeTest.java | 3 +-- .../internal/operators/OperatorPublishTest.java | 2 +- .../internal/operators/OperatorSerializeTest.java | 2 +- .../operators/OperatorTakeUntilPredicateTest.java | 2 +- .../rx/internal/operators/OperatorZipTest.java | 15 +++++---------- .../rx/internal/util/IndexedRingBufferTest.java | 7 ++----- .../java/rx/observers/SerializedObserverTest.java | 2 +- 26 files changed, 36 insertions(+), 57 deletions(-) diff --git a/src/main/java/rx/internal/operators/BlockingOperatorNext.java b/src/main/java/rx/internal/operators/BlockingOperatorNext.java index 05b5b8f1d8..387f1c793b 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorNext.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorNext.java @@ -79,11 +79,11 @@ public boolean hasNext() { } // Since an iterator should not be used in different thread, // so we do not need any synchronization. - if (hasNext == false) { + if (!hasNext) { // the iterator has reached the end. return false; } - if (isNextConsumed == false) { + if (!isNextConsumed) { // next has not been used yet. return true; } diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 48521d00b1..6420e66451 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -195,7 +195,7 @@ public void call(final Subscriber child) { final AtomicBoolean resumeBoundary = new AtomicBoolean(true); // incremented when requests are made, decremented when requests are fulfilled - final AtomicLong consumerCapacity = new AtomicLong(0l); + final AtomicLong consumerCapacity = new AtomicLong(); final Scheduler.Worker worker = scheduler.createWorker(); child.add(worker); diff --git a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java index d0bfdb1dbb..e08fd440c2 100644 --- a/src/main/java/rx/internal/operators/OperatorBufferWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorBufferWithSize.java @@ -156,7 +156,6 @@ public void request(long n) { } if (n == Long.MAX_VALUE) { requestInfinite(); - return; } else { if (firstRequest) { firstRequest = false; diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index e91e669bba..ad79f5894f 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -229,5 +229,5 @@ public void setProducer(Producer producer) { arbiter.setProducer(producer); } - }; + } } diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java index 62763f1948..e5aae95aaa 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java @@ -117,7 +117,6 @@ public void onNext(T t) { noWindow = true; if (child.isUnsubscribed()) { unsubscribe(); - return; } } } diff --git a/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java b/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java index 95a4c30561..67b22fb982 100644 --- a/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java +++ b/src/main/java/rx/internal/operators/OperatorWithLatestFrom.java @@ -60,7 +60,6 @@ public void onNext(T t) { s.onNext(result); } catch (Throwable e) { Exceptions.throwOrReport(e, this); - return; } } } diff --git a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java index d901304680..76d3f95926 100644 --- a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java @@ -160,18 +160,16 @@ public Subscription schedule(Action0 action) { if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } - ScheduledAction s = poolWorker.scheduleActual(action, 0, null, serial); - - return s; + + return poolWorker.scheduleActual(action, 0, null, serial); } @Override public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { if (isUnsubscribed()) { return Subscriptions.unsubscribed(); } - ScheduledAction s = poolWorker.scheduleActual(action, delayTime, unit, timed); - - return s; + + return poolWorker.scheduleActual(action, delayTime, unit, timed); } } diff --git a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java index e4c3e9ba61..8d0d5bdec2 100644 --- a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java +++ b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java @@ -70,7 +70,6 @@ public void start() { NewThreadWorker.registerExecutor((ScheduledThreadPoolExecutor)exec); } } - return; } else { exec.shutdownNow(); } diff --git a/src/main/java/rx/internal/util/SubscriptionRandomList.java b/src/main/java/rx/internal/util/SubscriptionRandomList.java index 8883861cd4..963442c230 100644 --- a/src/main/java/rx/internal/util/SubscriptionRandomList.java +++ b/src/main/java/rx/internal/util/SubscriptionRandomList.java @@ -108,7 +108,7 @@ public void clear() { } public void forEach(Action1 action) { - T[] ss=null; + T[] ss = null; synchronized (this) { if (unsubscribed || subscriptions == null) { return; diff --git a/src/main/java/rx/observables/AbstractOnSubscribe.java b/src/main/java/rx/observables/AbstractOnSubscribe.java index 1a1526766e..6becdc50a3 100644 --- a/src/main/java/rx/observables/AbstractOnSubscribe.java +++ b/src/main/java/rx/observables/AbstractOnSubscribe.java @@ -597,10 +597,7 @@ protected boolean use() { */ protected void free() { int i = inUse.get(); - if (i <= 0) { - return; - } else - if (inUse.decrementAndGet() == 0) { + if (i > 0 && inUse.decrementAndGet() == 0) { parent.onTerminated(state); } } diff --git a/src/main/java/rx/observables/AsyncOnSubscribe.java b/src/main/java/rx/observables/AsyncOnSubscribe.java index 84cb4c98e4..61cbe79e7c 100644 --- a/src/main/java/rx/observables/AsyncOnSubscribe.java +++ b/src/main/java/rx/observables/AsyncOnSubscribe.java @@ -29,7 +29,7 @@ import rx.observers.*; import rx.plugins.RxJavaPlugins; import rx.subscriptions.CompositeSubscription; -; + /** * A utility class to create {@code OnSubscribe} functions that respond correctly to back * pressure requests from subscribers. This is an improvement over diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index f2230f4bba..d60e147b5f 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -958,7 +958,7 @@ public void evictFinal(NodeList t1) { public boolean test(Object value, long now) { return first.test(value, now) || second.test(value, now); } - }; + } /** Maps the values to Timestamped. */ static final class AddTimestamped implements Func1 { diff --git a/src/main/java/rx/subscriptions/RefCountSubscription.java b/src/main/java/rx/subscriptions/RefCountSubscription.java index af225fa1a7..9d5434869f 100644 --- a/src/main/java/rx/subscriptions/RefCountSubscription.java +++ b/src/main/java/rx/subscriptions/RefCountSubscription.java @@ -143,5 +143,5 @@ public void unsubscribe() { public boolean isUnsubscribed() { return innerDone != 0; } - }; + } } diff --git a/src/test/java/rx/CovarianceTest.java b/src/test/java/rx/CovarianceTest.java index 6c60d8a73f..6b7a374be8 100644 --- a/src/test/java/rx/CovarianceTest.java +++ b/src/test/java/rx/CovarianceTest.java @@ -211,7 +211,7 @@ public Observable call(List> listOfLists) { return Observable.from(delta); } - }; + } }; /* diff --git a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java index e593d30465..c28606cae0 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java @@ -251,7 +251,7 @@ public void testCombineLatest3TypesB() { } private Func3 getConcat3StringsCombineLatestFunction() { - Func3 combineLatestFunction = new Func3() { + return new Func3() { @Override public String call(String a1, String a2, String a3) { @@ -268,11 +268,10 @@ public String call(String a1, String a2, String a3) { } }; - return combineLatestFunction; } private Func2 getConcatStringIntegerCombineLatestFunction() { - Func2 combineLatestFunction = new Func2() { + return new Func2() { @Override public String call(String s, Integer i) { @@ -280,11 +279,10 @@ public String call(String s, Integer i) { } }; - return combineLatestFunction; } private Func3 getConcatStringIntegerIntArrayCombineLatestFunction() { - Func3 combineLatestFunction = new Func3() { + return new Func3() { @Override public String call(String s, Integer i, int[] iArray) { @@ -292,7 +290,6 @@ public String call(String s, Integer i, int[] iArray) { } }; - return combineLatestFunction; } private static String getStringValue(Object o) { diff --git a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java index 03d4cfd24a..0ee4192add 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeUsingTest.java @@ -45,9 +45,9 @@ public class OnSubscribeUsingTest { private interface Resource { - public String getTextFromWeb(); - - public void dispose(); + String getTextFromWeb(); + + void dispose(); } private static class DisposeAction implements Action1 { diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index c04b6dd910..bce41e0e16 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -749,7 +749,7 @@ public void onNext(Integer t) { if (counter.getAndIncrement() % 100 == 0) { System.out.print("testIssue2890NoStackoverflow -> "); System.out.println(counter.get()); - }; + } } @Override diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index b14b7ad373..43aad7eedf 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -976,7 +976,7 @@ public String toString() { Observable ASYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) { return SYNC_INFINITE_OBSERVABLE_OF_EVENT(numGroups, subscribeCounter, sentEventCounter).subscribeOn(Schedulers.newThread()); - }; + } Observable SYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) { return Observable.create(new OnSubscribe() { @@ -997,7 +997,7 @@ public void call(final Subscriber op) { } }); - }; + } @Test public void testGroupByOnAsynchronousSourceAcceptsMultipleSubscriptions() throws InterruptedException { diff --git a/src/test/java/rx/internal/operators/OperatorMapTest.java b/src/test/java/rx/internal/operators/OperatorMapTest.java index bcca50fab8..d79d5863b6 100644 --- a/src/test/java/rx/internal/operators/OperatorMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorMapTest.java @@ -91,7 +91,7 @@ public void testMapMany() { @Override public Observable call(Integer id) { /* simulate making a nested async call which creates another Observable */ - Observable> subObservable = null; + Observable> subObservable; if (id == 1) { Map m1 = getMap("One"); Map m2 = getMap("Two"); diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 9732611e44..ab6123b067 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -946,7 +946,7 @@ public Observable call(Integer i) { } private Observable createInfiniteObservable(final AtomicInteger generated) { - Observable observable = Observable.from(new Iterable() { + return Observable.from(new Iterable() { @Override public Iterator iterator() { return new Iterator() { @@ -967,7 +967,6 @@ public boolean hasNext() { }; } }); - return observable; } @Test diff --git a/src/test/java/rx/internal/operators/OperatorPublishTest.java b/src/test/java/rx/internal/operators/OperatorPublishTest.java index 8f9b25a325..f247638891 100644 --- a/src/test/java/rx/internal/operators/OperatorPublishTest.java +++ b/src/test/java/rx/internal/operators/OperatorPublishTest.java @@ -199,7 +199,7 @@ public void call() { sourceUnsubscribed.set(true); } }).share(); - ; + final AtomicBoolean child1Unsubscribed = new AtomicBoolean(); final AtomicBoolean child2Unsubscribed = new AtomicBoolean(); diff --git a/src/test/java/rx/internal/operators/OperatorSerializeTest.java b/src/test/java/rx/internal/operators/OperatorSerializeTest.java index faed052beb..f126307af5 100644 --- a/src/test/java/rx/internal/operators/OperatorSerializeTest.java +++ b/src/test/java/rx/internal/operators/OperatorSerializeTest.java @@ -213,7 +213,7 @@ public void run() { } } - private static enum TestConcurrencyobserverEvent { + private enum TestConcurrencyobserverEvent { onCompleted, onError, onNext } diff --git a/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java index 0bcf4757f7..dd56392147 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeUntilPredicateTest.java @@ -30,7 +30,7 @@ import rx.functions.Func1; import rx.internal.util.UtilityFunctions; import rx.observers.TestSubscriber; -; + public class OperatorTakeUntilPredicateTest { @Test diff --git a/src/test/java/rx/internal/operators/OperatorZipTest.java b/src/test/java/rx/internal/operators/OperatorZipTest.java index d9487c5b03..dcf323da7e 100644 --- a/src/test/java/rx/internal/operators/OperatorZipTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipTest.java @@ -594,7 +594,7 @@ public String call(String t1, String t2) { } private Func2 getDivideZipr() { - Func2 zipr = new Func2() { + return new Func2() { @Override public Integer call(Integer i1, Integer i2) { @@ -602,11 +602,10 @@ public Integer call(Integer i1, Integer i2) { } }; - return zipr; } private Func3 getConcat3StringsZipr() { - Func3 zipr = new Func3() { + return new Func3() { @Override public String call(String a1, String a2, String a3) { @@ -623,11 +622,10 @@ public String call(String a1, String a2, String a3) { } }; - return zipr; } private Func2 getConcatStringIntegerZipr() { - Func2 zipr = new Func2() { + return new Func2() { @Override public String call(String s, Integer i) { @@ -635,11 +633,10 @@ public String call(String s, Integer i) { } }; - return zipr; } private Func3 getConcatStringIntegerIntArrayZipr() { - Func3 zipr = new Func3() { + return new Func3() { @Override public String call(String s, Integer i, int[] iArray) { @@ -647,7 +644,6 @@ public String call(String s, Integer i, int[] iArray) { } }; - return zipr; } private static String getStringValue(Object o) { @@ -1147,7 +1143,7 @@ public String call(Integer t1, Integer t2) { } private Observable createInfiniteObservable(final AtomicInteger generated) { - Observable observable = Observable.from(new Iterable() { + return Observable.from(new Iterable() { @Override public Iterator iterator() { return new Iterator() { @@ -1168,7 +1164,6 @@ public boolean hasNext() { }; } }); - return observable; } Observable OBSERVABLE_OF_5_INTEGERS = OBSERVABLE_OF_5_INTEGERS(new AtomicInteger()); diff --git a/src/test/java/rx/internal/util/IndexedRingBufferTest.java b/src/test/java/rx/internal/util/IndexedRingBufferTest.java index d0472583c9..5417693b85 100644 --- a/src/test/java/rx/internal/util/IndexedRingBufferTest.java +++ b/src/test/java/rx/internal/util/IndexedRingBufferTest.java @@ -191,11 +191,8 @@ public Boolean call(String t1) { @Override public Boolean call(String t1) { list.add(t1); - if (i++ == 2) { - return false; - } else { - return true; - } + i++; + return i != 3; } }, 0); diff --git a/src/test/java/rx/observers/SerializedObserverTest.java b/src/test/java/rx/observers/SerializedObserverTest.java index a14f146e75..7f833dda28 100644 --- a/src/test/java/rx/observers/SerializedObserverTest.java +++ b/src/test/java/rx/observers/SerializedObserverTest.java @@ -500,7 +500,7 @@ public void run() { } } - private static enum TestConcurrencyObserverEvent { + private enum TestConcurrencyObserverEvent { onCompleted, onError, onNext } From 2649b68a6ab799cb666a8246d14ed86ee53a62d5 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 9 Nov 2015 14:53:10 +0100 Subject: [PATCH 435/857] Test fixes to avoid problems with Android emulator-based testing. --- src/test/java/rx/ObservableTests.java | 102 +++++++++--------- .../OnSubscribeCombineLatestTest.java | 1 + .../operators/OnSubscribeRefCountTest.java | 21 +++- .../OperatorMergeMaxConcurrentTest.java | 7 +- .../operators/OperatorReplayTest.java | 9 +- 5 files changed, 86 insertions(+), 54 deletions(-) diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index d59e8c41a9..99dfd9e89c 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -15,49 +15,25 @@ */ package rx; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.isA; -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.LinkedList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable.OnSubscribe; -import rx.Observable.Transformer; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.*; + +import rx.Observable.*; import rx.exceptions.OnErrorNotImplementedException; -import rx.functions.Action1; -import rx.functions.Action2; -import rx.functions.Func0; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.functions.*; import rx.observables.ConnectableObservable; import rx.observers.TestSubscriber; -import rx.schedulers.TestScheduler; -import rx.subjects.ReplaySubject; -import rx.subjects.Subject; +import rx.schedulers.*; +import rx.subjects.*; import rx.subscriptions.BooleanSubscription; public class ObservableTests { @@ -1101,19 +1077,45 @@ public String call(Integer t1) { } @Test - public void testErrorThrownIssue1685() { + public void testErrorThrownIssue1685() throws Exception { Subject subject = ReplaySubject.create(); - Observable.error(new RuntimeException("oops")) - .materialize() - .delay(1, TimeUnit.SECONDS) - .dematerialize() - .subscribe(subject); - - subject.subscribe(); - subject.materialize().toBlocking().first(); + ExecutorService exec = Executors.newSingleThreadExecutor(); + + try { + + final AtomicReference err = new AtomicReference(); + + Scheduler s = Schedulers.from(exec); + exec.submit(new Runnable() { + @Override + public void run() { + Thread.currentThread().setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + err.set(e); + } + }); + } + }).get(); + + Observable.error(new RuntimeException("oops")) + .materialize() + .delay(1, TimeUnit.SECONDS, s) + .dematerialize() + .subscribe(subject); + + subject.subscribe(); + subject.materialize().toBlocking().first(); - System.out.println("Done"); + Thread.sleep(1000); // the uncaught exception comes after the terminal event reaches toBlocking + + assertNotNull("UncaughtExceptionHandler didn't get anything.", err.get()); + + System.out.println("Done"); + } finally { + exec.shutdownNow(); + } } @Test diff --git a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java index c28606cae0..bf4af98057 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java @@ -818,6 +818,7 @@ public void testWithCombineLatestIssue1717() throws InterruptedException { final AtomicInteger count = new AtomicInteger(); final int SIZE = 2000; Observable timer = Observable.interval(0, 1, TimeUnit.MILLISECONDS) + .onBackpressureBuffer() .observeOn(Schedulers.newThread()) .doOnEach(new Action1>() { diff --git a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java index fa38d2bdf1..ab076ce411 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeRefCountTest.java @@ -21,14 +21,14 @@ import java.util.*; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.*; import org.junit.*; import org.mockito.*; import rx.*; -import rx.Observable.OnSubscribe; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.Observer; import rx.functions.*; import rx.observers.*; @@ -528,6 +528,10 @@ public Integer call(Integer t1, Integer t2) { @Test(timeout = 10000) public void testUpstreamErrorAllowsRetry() throws InterruptedException { + + final AtomicReference err1 = new AtomicReference(); + final AtomicReference err2 = new AtomicReference(); + final AtomicInteger intervalSubscribed = new AtomicInteger(); Observable interval = Observable.interval(200,TimeUnit.MILLISECONDS) @@ -572,6 +576,11 @@ public void call(Throwable t1) { public void call(String t1) { System.out.println("Subscriber 1: " + t1); } + }, new Action1() { + @Override + public void call(Throwable t) { + err1.set(t); + } }); Thread.sleep(100); interval @@ -587,11 +596,19 @@ public void call(Throwable t1) { public void call(String t1) { System.out.println("Subscriber 2: " + t1); } + }, new Action1() { + @Override + public void call(Throwable t) { + err2.set(t); + } }); Thread.sleep(1300); System.out.println(intervalSubscribed.get()); assertEquals(6, intervalSubscribed.get()); + + assertNotNull("First subscriber didn't get the error", err1); + assertNotNull("Second subscriber didn't get the error", err2); } } diff --git a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java index af20d14316..128af7cb8e 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeMaxConcurrentTest.java @@ -27,6 +27,7 @@ import rx.*; import rx.Observable; import rx.Observer; +import rx.internal.util.PlatformDependent; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -218,7 +219,11 @@ public void testSimpleAsync() { } @Test(timeout = 10000) public void testSimpleOneLessAsyncLoop() { - for (int i = 0; i < 200; i++) { + int max = 200; + if (PlatformDependent.isAndroid()) { + max = 50; + } + for (int i = 0; i < max; i++) { testSimpleOneLessAsync(); } } diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index c0ec384d84..408fbc71ba 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -46,6 +46,7 @@ import rx.internal.operators.OperatorReplay.BoundedReplayBuffer; import rx.internal.operators.OperatorReplay.Node; import rx.internal.operators.OperatorReplay.SizeAndTimeBoundReplayBuffer; +import rx.internal.util.PlatformDependent; import rx.observables.ConnectableObservable; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -1049,7 +1050,13 @@ public void testAsyncComeAndGo() { @Test public void testNoMissingBackpressureException() { - final int m = 4 * 1000 * 1000; + final int m; + if (PlatformDependent.isAndroid()) { + m = 500 * 1000; + } else { + m = 4 * 1000 * 1000; + } + Observable firehose = Observable.create(new OnSubscribe() { @Override public void call(Subscriber t) { From c89ea3238b0e56b22a12f8a14a6e4e94c6be6a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 9 Nov 2015 21:56:59 +0100 Subject: [PATCH 436/857] 1.x: eager concatMap to choose safe or unsafe queue based on platform. I forgot to add the choice because 2.x SpscArrayQueue doesn't use Unsafe. I copied the SpscAtomicArrayQueue from #3169 and I hope it won't conflict. --- .../operators/OperatorEagerConcatMap.java | 11 +- .../atomic/AtomicReferenceArrayQueue.java | 75 +++++++++++ .../util/atomic/SpscAtomicArrayQueue.java | 124 ++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java create mode 100644 src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java diff --git a/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java index 127f2fbd51..4df115b7ae 100644 --- a/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java +++ b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java @@ -24,7 +24,8 @@ import rx.Observable.Operator; import rx.exceptions.Exceptions; import rx.functions.*; -import rx.internal.util.unsafe.SpscArrayQueue; +import rx.internal.util.atomic.SpscAtomicArrayQueue; +import rx.internal.util.unsafe.*; import rx.subscriptions.Subscriptions; public final class OperatorEagerConcatMap implements Operator { @@ -278,7 +279,13 @@ static final class EagerInnerSubscriber extends Subscriber { public EagerInnerSubscriber(EagerOuterSubscriber parent, int bufferSize) { super(); this.parent = parent; - this.queue = new SpscArrayQueue(bufferSize); + Queue q; + if (UnsafeAccess.isUnsafeAvailable()) { + q = new SpscArrayQueue(bufferSize); + } else { + q = new SpscAtomicArrayQueue(bufferSize); + } + this.queue = q; request(bufferSize); } diff --git a/src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java b/src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java new file mode 100644 index 0000000000..f7594ba20a --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java @@ -0,0 +1,75 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicReferenceArrayQueue.java + */ +package rx.internal.util.atomic; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReferenceArray; + +import rx.internal.util.unsafe.Pow2; + +abstract class AtomicReferenceArrayQueue extends AbstractQueue { + protected final AtomicReferenceArray buffer; + protected final int mask; + public AtomicReferenceArrayQueue(int capacity) { + int actualCapacity = Pow2.roundToPowerOfTwo(capacity); + this.mask = actualCapacity - 1; + this.buffer = new AtomicReferenceArray(actualCapacity); + } + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + @Override + public void clear() { + // we have to test isEmpty because of the weaker poll() guarantee + while (poll() != null || !isEmpty()) + ; + } + protected final int calcElementOffset(long index, int mask) { + return (int)index & mask; + } + protected final int calcElementOffset(long index) { + return (int)index & mask; + } + protected final E lvElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); + } + protected final E lpElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); // no weaker form available + } + protected final E lpElement(int offset) { + return buffer.get(offset); // no weaker form available + } + protected final void spElement(AtomicReferenceArray buffer, int offset, E value) { + buffer.lazySet(offset, value); // no weaker form available + } + protected final void spElement(int offset, E value) { + buffer.lazySet(offset, value); // no weaker form available + } + protected final void soElement(AtomicReferenceArray buffer, int offset, E value) { + buffer.lazySet(offset, value); + } + protected final void soElement(int offset, E value) { + buffer.lazySet(offset, value); + } + protected final void svElement(AtomicReferenceArray buffer, int offset, E value) { + buffer.set(offset, value); + } + protected final E lvElement(int offset) { + return lvElement(buffer, offset); + } +} diff --git a/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java new file mode 100644 index 0000000000..65c29e3ce8 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java @@ -0,0 +1,124 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/SpscAtomicArrayQueue.java + */ +package rx.internal.util.atomic; + +import java.util.concurrent.atomic.*; + +/** + * A Single-Producer-Single-Consumer queue backed by a pre-allocated buffer. + *

+ * This implementation is a mashup of the Fast Flow + * algorithm with an optimization of the offer method taken from the BQueue algorithm (a variation on Fast + * Flow), and adjusted to comply with Queue.offer semantics with regards to capacity.
+ * For convenience the relevant papers are available in the resources folder:
+ * 2010 - Pisa - SPSC Queues on Shared Cache Multi-Core Systems.pdf
+ * 2012 - Junchang- BQueue- Efficient and Practical Queuing.pdf
+ *
This implementation is wait free. + * + * @param + */ +public final class SpscAtomicArrayQueue extends AtomicReferenceArrayQueue { + private static final Integer MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); + final AtomicLong producerIndex; + protected long producerLookAhead; + final AtomicLong consumerIndex; + final int lookAheadStep; + public SpscAtomicArrayQueue(int capacity) { + super(capacity); + this.producerIndex = new AtomicLong(); + this.consumerIndex = new AtomicLong(); + lookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + @Override + public boolean offer(E e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = this.buffer; + final int mask = this.mask; + final long index = producerIndex.get(); + final int offset = calcElementOffset(index, mask); + if (index >= producerLookAhead) { + int step = lookAheadStep; + if (null == lvElement(buffer, calcElementOffset(index + step, mask))) {// LoadLoad + producerLookAhead = index + step; + } + else if (null != lvElement(buffer, offset)){ + return false; + } + } + soProducerIndex(index + 1); // ordered store -> atomic and ordered for size() + soElement(buffer, offset, e); // StoreStore + return true; + } + + @Override + public E poll() { + final long index = consumerIndex.get(); + final int offset = calcElementOffset(index); + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray lElementBuffer = buffer; + final E e = lvElement(lElementBuffer, offset);// LoadLoad + if (null == e) { + return null; + } + soConsumerIndex(index + 1); // ordered store -> atomic and ordered for size() + soElement(lElementBuffer, offset, null);// StoreStore + return e; + } + + @Override + public E peek() { + return lvElement(calcElementOffset(consumerIndex.get())); + } + + @Override + public int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and consumer + * indices, therefore protection is required to ensure size is within valid range. In the event of concurrent + * polls/offers to this method the size is OVER estimated as we read consumer index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + private void soProducerIndex(long newIndex) { + producerIndex.lazySet(newIndex); + } + + private void soConsumerIndex(long newIndex) { + consumerIndex.lazySet(newIndex); + } + + private long lvConsumerIndex() { + return consumerIndex.get(); + } + private long lvProducerIndex() { + return producerIndex.get(); + } +} From eba4df3d042cf7f80b97bb894360bda448c22682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 9 Nov 2015 22:44:04 +0100 Subject: [PATCH 437/857] 1.x: Remove unused and inefficient SubscriptionRandomList. Use the standard CompositeSubscription instead. --- .../internal/util/SubscriptionRandomList.java | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 src/main/java/rx/internal/util/SubscriptionRandomList.java diff --git a/src/main/java/rx/internal/util/SubscriptionRandomList.java b/src/main/java/rx/internal/util/SubscriptionRandomList.java deleted file mode 100644 index 963442c230..0000000000 --- a/src/main/java/rx/internal/util/SubscriptionRandomList.java +++ /dev/null @@ -1,155 +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.util; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import rx.Subscription; -import rx.exceptions.Exceptions; -import rx.functions.Action1; - -/** - * Subscription that represents a group of Subscriptions that are unsubscribed together. - * - * @see Rx.Net equivalent CompositeDisposable - */ -public final class SubscriptionRandomList implements Subscription { - - private Set subscriptions; - private boolean unsubscribed = false; - - public SubscriptionRandomList() { - } - - @Override - public synchronized boolean isUnsubscribed() { - return unsubscribed; - } - - /** - * Adds a new {@link Subscription} to this {@code CompositeSubscription} if the {@code CompositeSubscription} is not yet unsubscribed. If the {@code CompositeSubscription} is - * unsubscribed, {@code add} will indicate this by explicitly unsubscribing the new {@code Subscription} as - * well. - * - * @param s - * the {@link Subscription} to add - */ - public void add(final T s) { - Subscription unsubscribe = null; - synchronized (this) { - if (unsubscribed) { - unsubscribe = s; - } else { - if (subscriptions == null) { - subscriptions = new HashSet(4); - } - subscriptions.add(s); - } - } - if (unsubscribe != null) { - // call after leaving the synchronized block so we're not holding a lock while executing this - unsubscribe.unsubscribe(); - } - } - - /** - * Removes a {@link Subscription} from this {@code CompositeSubscription}, and unsubscribes the {@link Subscription}. - * - * @param s - * the {@link Subscription} to remove - */ - public void remove(final Subscription s) { - boolean unsubscribe = false; - synchronized (this) { - if (unsubscribed || subscriptions == null) { - return; - } - unsubscribe = subscriptions.remove(s); - } - if (unsubscribe) { - // if we removed successfully we then need to call unsubscribe on it (outside of the lock) - s.unsubscribe(); - } - } - - /** - * Unsubscribes any subscriptions that are currently part of this {@code CompositeSubscription} and remove - * them from the {@code CompositeSubscription} so that the {@code CompositeSubscription} is empty and in - * an unoperative state. - */ - public void clear() { - Collection unsubscribe = null; - synchronized (this) { - if (unsubscribed || subscriptions == null) { - return; - } else { - unsubscribe = subscriptions; - subscriptions = null; - } - } - unsubscribeFromAll(unsubscribe); - } - - public void forEach(Action1 action) { - T[] ss = null; - synchronized (this) { - if (unsubscribed || subscriptions == null) { - return; - } - ss = subscriptions.toArray(ss); - } - for (T t : ss) { - action.call(t); - } - } - - @Override - public void unsubscribe() { - Collection unsubscribe = null; - synchronized (this) { - if (unsubscribed) { - return; - } - unsubscribed = true; - unsubscribe = subscriptions; - subscriptions = null; - } - // we will only get here once - unsubscribeFromAll(unsubscribe); - } - - private static void unsubscribeFromAll(Collection subscriptions) { - if (subscriptions == null) { - return; - } - List es = null; - for (T s : subscriptions) { - try { - s.unsubscribe(); - } catch (Throwable e) { - if (es == null) { - es = new ArrayList(); - } - es.add(e); - } - } - Exceptions.throwIfAny(es); - } -} From d153d71b2a37d62293df19c78175798a7634b3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 9 Nov 2015 22:58:54 +0100 Subject: [PATCH 438/857] 1.x: fix SafeSubscriber documentation regarding unsubscribe The documentation was wrong in two points: unsubscription doesn't call onCompleted and unsubscription doesn't directly prevent delivery of onXXX events since the implementation doesn't even check isUnsubscribed: (it is the responsibility of the upstream to do that). --- src/main/java/rx/observers/SafeSubscriber.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/observers/SafeSubscriber.java b/src/main/java/rx/observers/SafeSubscriber.java index 8a9aad5179..2baf0caaf9 100644 --- a/src/main/java/rx/observers/SafeSubscriber.java +++ b/src/main/java/rx/observers/SafeSubscriber.java @@ -50,7 +50,8 @@ *

    *
  • Allows only single execution of either {@code onError} or {@code onCompleted}.
  • *
  • Ensures that once an {@code onCompleted} or {@code onError} is performed, no further calls can be executed
  • - *
  • If {@code unsubscribe} is called, calls {@code onCompleted} and forbids any further {@code onNext} calls.
  • + *
  • If {@code unsubscribe} is called, the upstream {@code Observable} is notified and the event delivery will be stopped in a + * best effort manner (i.e., further onXXX calls may still slip through).
  • *
  • When {@code onError} or {@code onCompleted} occur, unsubscribes from the {@code Observable} (if executing asynchronously).
  • *
* {@code SafeSubscriber} will not synchronize {@code onNext} execution. Use {@link SerializedSubscriber} to do From d80729249fc9329880f254a29fb74b3e23c802cc Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 10 Nov 2015 08:53:40 +0100 Subject: [PATCH 439/857] 1.x: SyncOnSubscribeTest.testConcurrentRequests give more time. The test failed on Travis and locally if my machine was under heavy load without interacting with the mock. This change gives more time in the inner await and reports the exception instead of itself throwing. --- .../rx/observables/SyncOnSubscribeTest.java | 89 ++++++++----------- 1 file changed, 37 insertions(+), 52 deletions(-) diff --git a/src/test/java/rx/observables/SyncOnSubscribeTest.java b/src/test/java/rx/observables/SyncOnSubscribeTest.java index 22e1f11cfd..365e457c81 100644 --- a/src/test/java/rx/observables/SyncOnSubscribeTest.java +++ b/src/test/java/rx/observables/SyncOnSubscribeTest.java @@ -16,57 +16,25 @@ package rx.observables; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.isA; -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.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; +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.*; import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.Matchers; -import org.mockito.Mockito; +import org.mockito.*; +import rx.*; import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Observable.Operator; +import rx.Observable.*; import rx.Observer; -import rx.Producer; -import rx.Subscriber; import rx.exceptions.TestException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Action2; -import rx.functions.Func0; -import rx.functions.Func2; +import rx.functions.*; import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.schedulers.TestScheduler; +import rx.schedulers.*; /** * Test if SyncOnSubscribe adheres to the usual unsubscription and backpressure contracts. @@ -489,6 +457,16 @@ public Integer call(Integer state, Observer observer) { verify(onUnSubscribe, times(1)).call(any(Integer.class)); } + @Test + public void testConcurrentRequestsLoop() throws InterruptedException { + for (int i = 0; i < 100; i++) { + if (i % 10 == 0) { + System.out.println("testConcurrentRequestsLoop >> " + i); + } + testConcurrentRequests(); + } + } + @Test public void testConcurrentRequests() throws InterruptedException { final int count1 = 1000; @@ -514,12 +492,20 @@ public Integer call(Integer state, Observer observer) { l2.countDown(); // wait until the 2nd request returns then proceed try { - if (!l1.await(1, TimeUnit.SECONDS)) - throw new IllegalStateException(); - } catch (InterruptedException e) {} + if (!l1.await(2, TimeUnit.SECONDS)) { + observer.onError(new TimeoutException()); + return state + 1; + } + } catch (InterruptedException e) { + observer.onError(e); + return state + 1; + } observer.onNext(state); - if (state == finalCount) + + if (state == finalCount) { observer.onCompleted(); + } + return state + 1; }}, onUnSubscribe); @@ -532,10 +518,9 @@ public Integer call(Integer state, Observer observer) { Observable.create(os).subscribeOn(Schedulers.newThread()).subscribe(ts); // wait until the first request has started processing - try { - if (!l2.await(1, TimeUnit.SECONDS)) - throw new IllegalStateException(); - } catch (InterruptedException e) {} + if (!l2.await(2, TimeUnit.SECONDS)) { + fail("SyncOnSubscribe failed to countDown in time"); + } // make a concurrent request, this should return ts.requestMore(count2); // unblock the 1st thread to proceed fulfilling requests From e8beca72a0378b15503e7ee3a0c1be9427e2eb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Tue, 10 Nov 2015 22:30:08 +0100 Subject: [PATCH 440/857] 1.x: merge can now run in horizontally unbounded mode. --- .../rx/internal/operators/OperatorMerge.java | 48 +-- .../atomic/AtomicReferenceArrayQueue.java | 75 ++++ .../util/atomic/SpscAtomicArrayQueue.java | 124 +++++++ .../atomic/SpscExactAtomicArrayQueue.java | 169 ++++++++++ .../atomic/SpscUnboundedAtomicArrayQueue.java | 319 ++++++++++++++++++ src/perf/java/rx/operators/FlatMapPerf.java | 71 ++++ .../internal/operators/OperatorMergeTest.java | 33 +- .../rx/internal/util/JCToolsQueueTests.java | 108 ++++++ 8 files changed, 923 insertions(+), 24 deletions(-) create mode 100644 src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java create mode 100644 src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java create mode 100644 src/main/java/rx/internal/util/atomic/SpscExactAtomicArrayQueue.java create mode 100644 src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java create mode 100644 src/perf/java/rx/operators/FlatMapPerf.java diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index d2f52cb204..3fd96791a0 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -17,13 +17,15 @@ import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.*; +import java.util.concurrent.atomic.AtomicLong; import rx.*; -import rx.Observable.Operator; import rx.Observable; +import rx.Observable.Operator; import rx.exceptions.*; import rx.internal.util.*; +import rx.internal.util.atomic.*; +import rx.internal.util.unsafe.*; import rx.subscriptions.CompositeSubscription; /** @@ -144,7 +146,7 @@ static final class MergeSubscriber extends Subscriber MergeProducer producer; - volatile RxRingBuffer queue; + volatile Queue queue; /** Tracks the active subscriptions to sources. */ volatile CompositeSubscription subscriptions; @@ -182,8 +184,7 @@ public MergeSubscriber(Subscriber child, boolean delayErrors, int max this.nl = NotificationLite.instance(); this.innerGuard = new Object(); this.innerSubscribers = EMPTY; - long r = Math.min(maxConcurrent, RxRingBuffer.SIZE); - request(r); + request(maxConcurrent == Integer.MAX_VALUE ? Long.MAX_VALUE : maxConcurrent); } Queue getOrCreateErrorQueue() { @@ -443,23 +444,27 @@ protected void queueScalar(T value) { * due to lack of requests or an ongoing emission, * enqueue the value and try the slow emission path. */ - RxRingBuffer q = this.queue; + Queue q = this.queue; if (q == null) { - q = RxRingBuffer.getSpscInstance(); - this.add(q); + int mc = maxConcurrent; + if (mc == Integer.MAX_VALUE) { + q = new SpscUnboundedAtomicArrayQueue(RxRingBuffer.SIZE); + } else { + if (Pow2.isPowerOfTwo(mc)) { + if (UnsafeAccess.isUnsafeAvailable()) { + q = new SpscArrayQueue(mc); + } else { + q = new SpscAtomicArrayQueue(mc); + } + } else { + q = new SpscExactAtomicArrayQueue(mc); + } + } this.queue = q; } - try { - q.onNext(nl.next(value)); - } catch (MissingBackpressureException ex) { - this.unsubscribe(); - this.onError(ex); - return; - } catch (IllegalStateException ex) { - if (!this.isUnsubscribed()) { - this.unsubscribe(); - this.onError(ex); - } + if (!q.offer(value)) { + unsubscribe(); + onError(OnErrorThrowable.addValueAsLastCause(new MissingBackpressureException(), value)); return; } emit(); @@ -533,7 +538,7 @@ void emitLoop() { skipFinal = true; return; } - RxRingBuffer svq = queue; + Queue svq = queue; long r = producer.get(); boolean unbounded = r == Long.MAX_VALUE; @@ -610,9 +615,6 @@ void emitLoop() { } else { reportError(); } - if (svq != null) { - svq.release(); - } skipFinal = true; return; } diff --git a/src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java b/src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java new file mode 100644 index 0000000000..f7594ba20a --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/AtomicReferenceArrayQueue.java @@ -0,0 +1,75 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/AtomicReferenceArrayQueue.java + */ +package rx.internal.util.atomic; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReferenceArray; + +import rx.internal.util.unsafe.Pow2; + +abstract class AtomicReferenceArrayQueue extends AbstractQueue { + protected final AtomicReferenceArray buffer; + protected final int mask; + public AtomicReferenceArrayQueue(int capacity) { + int actualCapacity = Pow2.roundToPowerOfTwo(capacity); + this.mask = actualCapacity - 1; + this.buffer = new AtomicReferenceArray(actualCapacity); + } + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + @Override + public void clear() { + // we have to test isEmpty because of the weaker poll() guarantee + while (poll() != null || !isEmpty()) + ; + } + protected final int calcElementOffset(long index, int mask) { + return (int)index & mask; + } + protected final int calcElementOffset(long index) { + return (int)index & mask; + } + protected final E lvElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); + } + protected final E lpElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); // no weaker form available + } + protected final E lpElement(int offset) { + return buffer.get(offset); // no weaker form available + } + protected final void spElement(AtomicReferenceArray buffer, int offset, E value) { + buffer.lazySet(offset, value); // no weaker form available + } + protected final void spElement(int offset, E value) { + buffer.lazySet(offset, value); // no weaker form available + } + protected final void soElement(AtomicReferenceArray buffer, int offset, E value) { + buffer.lazySet(offset, value); + } + protected final void soElement(int offset, E value) { + buffer.lazySet(offset, value); + } + protected final void svElement(AtomicReferenceArray buffer, int offset, E value) { + buffer.set(offset, value); + } + protected final E lvElement(int offset) { + return lvElement(buffer, offset); + } +} diff --git a/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java new file mode 100644 index 0000000000..65c29e3ce8 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java @@ -0,0 +1,124 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/SpscAtomicArrayQueue.java + */ +package rx.internal.util.atomic; + +import java.util.concurrent.atomic.*; + +/** + * A Single-Producer-Single-Consumer queue backed by a pre-allocated buffer. + *

+ * This implementation is a mashup of the Fast Flow + * algorithm with an optimization of the offer method taken from the BQueue algorithm (a variation on Fast + * Flow), and adjusted to comply with Queue.offer semantics with regards to capacity.
+ * For convenience the relevant papers are available in the resources folder:
+ * 2010 - Pisa - SPSC Queues on Shared Cache Multi-Core Systems.pdf
+ * 2012 - Junchang- BQueue- Efficient and Practical Queuing.pdf
+ *
This implementation is wait free. + * + * @param + */ +public final class SpscAtomicArrayQueue extends AtomicReferenceArrayQueue { + private static final Integer MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); + final AtomicLong producerIndex; + protected long producerLookAhead; + final AtomicLong consumerIndex; + final int lookAheadStep; + public SpscAtomicArrayQueue(int capacity) { + super(capacity); + this.producerIndex = new AtomicLong(); + this.consumerIndex = new AtomicLong(); + lookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + @Override + public boolean offer(E e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = this.buffer; + final int mask = this.mask; + final long index = producerIndex.get(); + final int offset = calcElementOffset(index, mask); + if (index >= producerLookAhead) { + int step = lookAheadStep; + if (null == lvElement(buffer, calcElementOffset(index + step, mask))) {// LoadLoad + producerLookAhead = index + step; + } + else if (null != lvElement(buffer, offset)){ + return false; + } + } + soProducerIndex(index + 1); // ordered store -> atomic and ordered for size() + soElement(buffer, offset, e); // StoreStore + return true; + } + + @Override + public E poll() { + final long index = consumerIndex.get(); + final int offset = calcElementOffset(index); + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray lElementBuffer = buffer; + final E e = lvElement(lElementBuffer, offset);// LoadLoad + if (null == e) { + return null; + } + soConsumerIndex(index + 1); // ordered store -> atomic and ordered for size() + soElement(lElementBuffer, offset, null);// StoreStore + return e; + } + + @Override + public E peek() { + return lvElement(calcElementOffset(consumerIndex.get())); + } + + @Override + public int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and consumer + * indices, therefore protection is required to ensure size is within valid range. In the event of concurrent + * polls/offers to this method the size is OVER estimated as we read consumer index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + private void soProducerIndex(long newIndex) { + producerIndex.lazySet(newIndex); + } + + private void soConsumerIndex(long newIndex) { + consumerIndex.lazySet(newIndex); + } + + private long lvConsumerIndex() { + return consumerIndex.get(); + } + private long lvProducerIndex() { + return producerIndex.get(); + } +} diff --git a/src/main/java/rx/internal/util/atomic/SpscExactAtomicArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscExactAtomicArrayQueue.java new file mode 100644 index 0000000000..00fc1f96f0 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/SpscExactAtomicArrayQueue.java @@ -0,0 +1,169 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/SpscAtomicArrayQueue.java + */ + +package rx.internal.util.atomic; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import rx.internal.util.unsafe.Pow2; + +/** + * A single-producer single-consumer bounded queue with exact capacity tracking. + *

This means that a queue of 10 will allow exactly 10 offers, however, the underlying storage is still power-of-2. + *

The implementation uses field updaters and thus should be platform-safe. + */ +public final class SpscExactAtomicArrayQueue extends AtomicReferenceArray implements Queue { + /** */ + private static final long serialVersionUID = 6210984603741293445L; + final int mask; + final int capacitySkip; + volatile long producerIndex; + volatile long consumerIndex; + + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater PRODUCER_INDEX = + AtomicLongFieldUpdater.newUpdater(SpscExactAtomicArrayQueue.class, "producerIndex"); + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater CONSUMER_INDEX = + AtomicLongFieldUpdater.newUpdater(SpscExactAtomicArrayQueue.class, "consumerIndex"); + + public SpscExactAtomicArrayQueue(int capacity) { + super(Pow2.roundToPowerOfTwo(capacity)); + int len = length(); + this.mask = len - 1; + this.capacitySkip = len - capacity; + } + + + @Override + public boolean offer(T value) { + if (value == null) { + throw new NullPointerException(); + } + + long pi = producerIndex; + int m = mask; + + int fullCheck = (int)(pi + capacitySkip) & m; + if (get(fullCheck) != null) { + return false; + } + int offset = (int)pi & m; + PRODUCER_INDEX.lazySet(this, pi + 1); + lazySet(offset, value); + return true; + } + @Override + public T poll() { + long ci = consumerIndex; + int offset = (int)ci & mask; + T value = get(offset); + if (value == null) { + return null; + } + CONSUMER_INDEX.lazySet(this, ci + 1); + lazySet(offset, null); + return value; + } + @Override + public T peek() { + return get((int)consumerIndex & mask); + } + @Override + public void clear() { + while (poll() != null || !isEmpty()); + } + @Override + public boolean isEmpty() { + return producerIndex == consumerIndex; + } + + @Override + public int size() { + long ci = consumerIndex; + for (;;) { + long pi = producerIndex; + long ci2 = consumerIndex; + if (ci == ci2) { + return (int)(pi - ci2); + } + ci = ci2; + } + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public E[] toArray(E[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(T e) { + throw new UnsupportedOperationException(); + } + + @Override + public T remove() { + throw new UnsupportedOperationException(); + } + + @Override + public T element() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java new file mode 100644 index 0000000000..af62a9ce60 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java @@ -0,0 +1,319 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/SpscUnboundedAtomicArrayQueue.java + */ + +package rx.internal.util.atomic; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import rx.internal.util.unsafe.Pow2; + +/** + * A single-producer single-consumer queue with unbounded capacity. + *

The implementation uses fixed, power-of-2 arrays to store elements and turns into a linked-list like + * structure if the production overshoots the consumption. + *

Note that the minimum capacity of the 'islands' are 8 due to how the look-ahead optimization works. + *

The implementation uses field updaters and thus should be platform-safe. + */ +public final class SpscUnboundedAtomicArrayQueue 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(SpscUnboundedAtomicArrayQueue.class, "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(SpscUnboundedAtomicArrayQueue.class, "consumerIndex"); + private static final Object HAS_NEXT = new Object(); + + public SpscUnboundedAtomicArrayQueue(final int bufferSize) { + int p2capacity = Pow2.roundToPowerOfTwo(Math.max(8, bufferSize)); // lookahead doesn't work with capacity < 8 + int mask = p2capacity - 1; + AtomicReferenceArray buffer = new AtomicReferenceArray(p2capacity + 1); + producerBuffer = buffer; + producerMask = mask; + adjustLookAheadStep(p2capacity); + consumerBuffer = buffer; + consumerMask = mask; + producerLookAhead = mask - 1; // we know it's all empty to start with + soProducerIndex(0L); + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single producer thread use only. + */ + @Override + public final boolean offer(final T e) { + if (e == null) { + throw new NullPointerException(); + } + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = producerBuffer; + final long index = lpProducerIndex(); + final int mask = producerMask; + final int offset = calcWrappedOffset(index, mask); + if (index < producerLookAhead) { + return writeToQueue(buffer, e, index, offset); + } else { + final int lookAheadStep = producerLookAheadStep; + // go around the buffer or resize if full (unless we hit max capacity) + int lookAheadElementOffset = calcWrappedOffset(index + lookAheadStep, mask); + if (null == lvElement(buffer, lookAheadElementOffset)) {// LoadLoad + producerLookAhead = index + lookAheadStep - 1; // joy, there's plenty of room + return writeToQueue(buffer, e, index, offset); + } else if (null != lvElement(buffer, calcWrappedOffset(index + 1, mask))) { // buffer is not full + return writeToQueue(buffer, e, index, offset); + } else { + resize(buffer, index, offset, e, mask); // add a buffer and link old to new + return true; + } + } + } + + private boolean writeToQueue(final AtomicReferenceArray buffer, final T e, final long index, final int offset) { + soProducerIndex(index + 1);// this ensures atomic write of long on 32bit platforms + soElement(buffer, offset, e);// StoreStore + return true; + } + + private void resize(final AtomicReferenceArray oldBuffer, final long currIndex, final int offset, final T e, + final long mask) { + final int capacity = oldBuffer.length(); + final AtomicReferenceArray newBuffer = new AtomicReferenceArray(capacity); + producerBuffer = newBuffer; + producerLookAhead = currIndex + mask - 1; + soProducerIndex(currIndex + 1);// this ensures correctness on 32bit platforms + soElement(newBuffer, offset, e);// StoreStore + soNext(oldBuffer, newBuffer); + soElement(oldBuffer, offset, HAS_NEXT); // new buffer is visible after element is + // inserted + } + + private void soNext(AtomicReferenceArray curr, AtomicReferenceArray next) { + soElement(curr, calcDirectOffset(curr.length() - 1), next); + } + @SuppressWarnings("unchecked") + private AtomicReferenceArray lvNext(AtomicReferenceArray curr) { + return (AtomicReferenceArray)lvElement(curr, calcDirectOffset(curr.length() - 1)); + } + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final T poll() { + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + boolean isNextBuffer = e == HAS_NEXT; + if (null != e && !isNextBuffer) { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(buffer, offset, null);// StoreStore + return (T) e; + } else if (isNextBuffer) { + return newBufferPoll(lvNext(buffer), index, mask); + } + + return null; + } + + @SuppressWarnings("unchecked") + private T newBufferPoll(AtomicReferenceArray nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + final T n = (T) lvElement(nextBuffer, offsetInNew);// LoadLoad + if (null == n) { + return null; + } else { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(nextBuffer, offsetInNew, null);// StoreStore + return n; + } + } + + /** + * {@inheritDoc} + *

+ * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final T peek() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + if (e == HAS_NEXT) { + return newBufferPeek(lvNext(buffer), index, mask); + } + + return (T) e; + } + + @Override + public void clear() { + while (poll() != null || !isEmpty()); + } + + @SuppressWarnings("unchecked") + private T newBufferPeek(AtomicReferenceArray nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + return (T) lvElement(nextBuffer, offsetInNew);// LoadLoad + } + + @Override + public final int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + @Override + public boolean isEmpty() { + return lvProducerIndex() == lvConsumerIndex(); + } + + private void adjustLookAheadStep(int capacity) { + producerLookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + private long lvProducerIndex() { + return producerIndex; + } + + private long lvConsumerIndex() { + return consumerIndex; + } + + private long lpProducerIndex() { + return producerIndex; + } + + private long lpConsumerIndex() { + return consumerIndex; + } + + private void soProducerIndex(long v) { + PRODUCER_INDEX.lazySet(this, v); + } + + private void soConsumerIndex(long v) { + CONSUMER_INDEX.lazySet(this, v); + } + + private static final int calcWrappedOffset(long index, int mask) { + return calcDirectOffset((int)index & mask); + } + private static final int calcDirectOffset(int index) { + return index; + } + private static final void soElement(AtomicReferenceArray buffer, int offset, Object e) { + buffer.lazySet(offset, e); + } + + private static final Object lvElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); + } + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public E[] toArray(E[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(T e) { + throw new UnsupportedOperationException(); + } + + @Override + public T remove() { + throw new UnsupportedOperationException(); + } + + @Override + public T element() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/perf/java/rx/operators/FlatMapPerf.java b/src/perf/java/rx/operators/FlatMapPerf.java new file mode 100644 index 0000000000..f8dafd467d --- /dev/null +++ b/src/perf/java/rx/operators/FlatMapPerf.java @@ -0,0 +1,71 @@ +/* + * Copyright 2011-2015 David Karnok + * + * 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 rx.Observable; +import rx.functions.Func1; + +/** + * Benchmark flatMap's optimizations. + *

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

+ * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*FlatMapPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlatMapPerf { + @Param({ "1", "1000", "1000000" }) + public int times; + + Observable rxSource; + Observable rxSource2; + + @Setup + public void setup() { + Observable rxRange = Observable.range(0, times); + rxSource = rxRange.flatMap(new Func1>() { + @Override + public Observable call(Integer t) { + return Observable.just(t); + } + }); + rxSource2 = rxRange.flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.range(v, 2); + } + }); + } + + @Benchmark + public Object rxFlatMap() { + return rxSource.subscribe(); + } + @Benchmark + public Object rxFlatMap2() { + return rxSource2.subscribe(); + } +} diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 9732611e44..c3ef0a83ee 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -716,7 +716,8 @@ public void onNext(Integer t) { } }; - Observable.merge(o1).observeOn(Schedulers.computation()).take(RxRingBuffer.SIZE * 2).subscribe(testSubscriber); + int limit = RxRingBuffer.SIZE; // the default unbounded behavior makes this test fail 100% of the time: source is too fast + Observable.merge(o1, limit).observeOn(Schedulers.computation()).take(RxRingBuffer.SIZE * 2).subscribe(testSubscriber); testSubscriber.awaitTerminalEvent(); if (testSubscriber.getOnErrorEvents().size() > 0) { testSubscriber.getOnErrorEvents().get(0).printStackTrace(); @@ -1303,4 +1304,34 @@ public void onNext(Integer t) { runMerge(toHiddenScalar, ts); } } + + @Test + public void testUnboundedDefaultConcurrency() { + List> os = new ArrayList>(); + for(int i=0; i < 2000; i++) { + os.add(Observable.never()); + } + os.add(Observable.range(0, 100)); + + TestSubscriber ts = TestSubscriber.create(); + Observable.merge(os).take(1).subscribe(ts); + ts.awaitTerminalEvent(5000, TimeUnit.MILLISECONDS); + ts.assertValue(0); + ts.assertCompleted(); + } + + @Test + public void testConcurrencyLimit() { + List> os = new ArrayList>(); + for(int i=0; i < 2000; i++) { + os.add(Observable.never()); + } + os.add(Observable.range(0, 100)); + + TestSubscriber ts = TestSubscriber.create(); + Observable.merge(os, Integer.MAX_VALUE).take(1).subscribe(ts); + ts.awaitTerminalEvent(5000, TimeUnit.MILLISECONDS); + ts.assertValue(0); + ts.assertCompleted(); + } } diff --git a/src/test/java/rx/internal/util/JCToolsQueueTests.java b/src/test/java/rx/internal/util/JCToolsQueueTests.java index fea60217eb..fdf844bf81 100644 --- a/src/test/java/rx/internal/util/JCToolsQueueTests.java +++ b/src/test/java/rx/internal/util/JCToolsQueueTests.java @@ -460,4 +460,112 @@ public void testUnsafeAccessAddressOf() { } UnsafeAccess.addressOf(Object.class, "field"); } + + @Test + public void testSpscExactAtomicArrayQueue() { + for (int i = 1; i <= RxRingBuffer.SIZE * 2; i++) { + SpscExactAtomicArrayQueue q = new SpscExactAtomicArrayQueue(i); + + for (int j = 0; j < i; j++) { + assertTrue(q.offer(j)); + } + + assertFalse(q.offer(i)); + + for (int j = 0; j < i; j++) { + assertEquals((Integer)j, q.peek()); + assertEquals((Integer)j, q.poll()); + } + + for (int j = 0; j < RxRingBuffer.SIZE * 4; j++) { + assertTrue(q.offer(j)); + assertEquals((Integer)j, q.peek()); + assertEquals((Integer)j, q.poll()); + } + } + } + + @Test + public void testUnboundedAtomicArrayQueue() { + for (int i = 1; i <= RxRingBuffer.SIZE * 2; i *= 2) { + SpscUnboundedAtomicArrayQueue q = new SpscUnboundedAtomicArrayQueue(i); + + for (int j = 0; j < i; j++) { + assertTrue(q.offer(j)); + } + + assertTrue(q.offer(i)); + + for (int j = 0; j < i; j++) { + assertEquals((Integer)j, q.peek()); + assertEquals((Integer)j, q.poll()); + } + + assertEquals((Integer)i, q.peek()); + assertEquals((Integer)i, q.poll()); + + for (int j = 0; j < RxRingBuffer.SIZE * 4; j++) { + assertTrue(q.offer(j)); + assertEquals((Integer)j, q.peek()); + assertEquals((Integer)j, q.poll()); + } + } + + } + + + @Test(expected = NullPointerException.class) + public void testSpscAtomicArrayQueueNull() { + SpscAtomicArrayQueue q = new SpscAtomicArrayQueue(16); + q.offer(null); + } + + @Test + public void testSpscAtomicArrayQueueOfferPoll() { + Queue q = new SpscAtomicArrayQueue(128); + + testOfferPoll(q); + } + @Test(expected = UnsupportedOperationException.class) + public void testSpscAtomicArrayQueueIterator() { + SpscAtomicArrayQueue q = new SpscAtomicArrayQueue(16); + q.iterator(); + } + + @Test(expected = NullPointerException.class) + public void testSpscExactAtomicArrayQueueNull() { + SpscExactAtomicArrayQueue q = new SpscExactAtomicArrayQueue(10); + q.offer(null); + } + + @Test + public void testSpscExactAtomicArrayQueueOfferPoll() { + Queue q = new SpscAtomicArrayQueue(120); + + testOfferPoll(q); + } + @Test(expected = UnsupportedOperationException.class) + public void testSpscExactAtomicArrayQueueIterator() { + SpscAtomicArrayQueue q = new SpscAtomicArrayQueue(10); + q.iterator(); + } + + @Test(expected = NullPointerException.class) + public void testSpscUnboundedAtomicArrayQueueNull() { + SpscUnboundedAtomicArrayQueue q = new SpscUnboundedAtomicArrayQueue(16); + q.offer(null); + } + + @Test + public void testSpscUnboundedAtomicArrayQueueOfferPoll() { + Queue q = new SpscUnboundedAtomicArrayQueue(128); + + testOfferPoll(q); + } + @Test(expected = UnsupportedOperationException.class) + public void testSpscUnboundedAtomicArrayQueueIterator() { + SpscUnboundedAtomicArrayQueue q = new SpscUnboundedAtomicArrayQueue(16); + q.iterator(); + } + } From d55cd407305d5303f7673dde9b18f09585c1e0f1 Mon Sep 17 00:00:00 2001 From: Ho Yan Leung Date: Sat, 29 Aug 2015 14:22:20 -0700 Subject: [PATCH 441/857] Implements BlockingSingle This commit adds BlockingSingle, the blocking version of rx.Single. BlockingSingle has the following methods: i `from(Single)` -- factory method for creating a `BlockingSingle` from a `Single` - `get()` -- returns the value emitted from the Single - `get(Func1 predicate)` -- returns the value if it matches the provided predicate - `toFuture()` -- returns a `java.util.concurrent.Future` Adds Single.toBlocking --- src/main/java/rx/Single.java | 16 +++ .../java/rx/internal/util/BlockingUtils.java | 59 ++++++++++ .../rx/observables/BlockingObservable.java | 26 +---- src/main/java/rx/singles/BlockingSingle.java | 106 ++++++++++++++++++ src/test/java/rx/SingleTest.java | 10 ++ .../rx/internal/util/BlockingUtilsTest.java | 105 +++++++++++++++++ .../java/rx/singles/BlockingSingleTest.java | 80 +++++++++++++ 7 files changed, 380 insertions(+), 22 deletions(-) create mode 100644 src/main/java/rx/internal/util/BlockingUtils.java create mode 100644 src/main/java/rx/singles/BlockingSingle.java create mode 100644 src/test/java/rx/internal/util/BlockingUtilsTest.java create mode 100644 src/test/java/rx/singles/BlockingSingleTest.java diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index e47f9c40c7..b126fd39a3 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -41,6 +41,7 @@ import rx.internal.operators.OperatorTimeout; import rx.internal.operators.OperatorZip; import rx.internal.producers.SingleDelayedProducer; +import rx.singles.BlockingSingle; import rx.observers.SafeSubscriber; import rx.plugins.RxJavaObservableExecutionHook; import rx.plugins.RxJavaPlugins; @@ -1794,6 +1795,21 @@ public final Single timeout(long timeout, TimeUnit timeUnit, Single(timeout, timeUnit, asObservable(other), scheduler)); } + /** + * Converts a Single into a {@link BlockingSingle} (a Single with blocking operators). + *

+ *
Scheduler:
+ *
{@code toBlocking} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @return a {@code BlockingSingle} version of this Single. + * @see ReactiveX operators documentation: To + */ + @Experimental + public final BlockingSingle toBlocking() { + return BlockingSingle.from(this); + } + /** * Returns a Single that emits the result of applying a specified function to the pair of items emitted by * the source Single and another specified Single. diff --git a/src/main/java/rx/internal/util/BlockingUtils.java b/src/main/java/rx/internal/util/BlockingUtils.java new file mode 100644 index 0000000000..951e49c83d --- /dev/null +++ b/src/main/java/rx/internal/util/BlockingUtils.java @@ -0,0 +1,59 @@ +/** + * Copyright 2015 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.Subscription; +import rx.annotations.Experimental; + +import java.util.concurrent.CountDownLatch; + +/** + * Utility functions relating to blocking types. + *

+ * Not intended to be part of the public API. + */ +@Experimental +public final class BlockingUtils { + + private BlockingUtils() { } + + /** + * Blocks and waits for a {@link Subscription} to complete. + * + * @param latch a CountDownLatch + * @param subscription the Subscription to wait on. + */ + @Experimental + public static void awaitForComplete(CountDownLatch latch, Subscription subscription) { + if (latch.getCount() == 0) { + // Synchronous observable completes before awaiting for it. + // Skip await so InterruptedException will never be thrown. + return; + } + // block until the subscription completes and then return + try { + latch.await(); + } catch (InterruptedException e) { + subscription.unsubscribe(); + // set the interrupted flag again so callers can still get it + // for more information see https://github.com/ReactiveX/RxJava/pull/147#issuecomment-13624780 + Thread.currentThread().interrupt(); + // using Runtime so it is not checked + throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); + } + } +} diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index 5463e9696e..c1ded4c217 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -26,6 +26,7 @@ import rx.exceptions.OnErrorNotImplementedException; import rx.functions.*; import rx.internal.operators.*; +import rx.internal.util.BlockingUtils; import rx.internal.util.UtilityFunctions; import rx.subscriptions.Subscriptions; @@ -123,7 +124,7 @@ public void onNext(T args) { onNext.call(args); } }); - awaitForComplete(latch, subscription); + BlockingUtils.awaitForComplete(latch, subscription); if (exceptionFromOnError.get() != null) { if (exceptionFromOnError.get() instanceof RuntimeException) { @@ -446,7 +447,7 @@ public void onNext(final T item) { returnItem.set(item); } }); - awaitForComplete(latch, subscription); + BlockingUtils.awaitForComplete(latch, subscription); if (returnException.get() != null) { if (returnException.get() instanceof RuntimeException) { @@ -458,25 +459,6 @@ public void onNext(final T item) { return returnItem.get(); } - - private void awaitForComplete(CountDownLatch latch, Subscription subscription) { - if (latch.getCount() == 0) { - // Synchronous observable completes before awaiting for it. - // Skip await so InterruptedException will never be thrown. - return; - } - // block until the subscription completes and then return - try { - latch.await(); - } catch (InterruptedException e) { - subscription.unsubscribe(); - // set the interrupted flag again so callers can still get it - // for more information see https://github.com/ReactiveX/RxJava/pull/147#issuecomment-13624780 - Thread.currentThread().interrupt(); - // using Runtime so it is not checked - throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); - } - } /** * Runs the source observable to a terminal event, ignoring any values and rethrowing any exception. @@ -502,7 +484,7 @@ public void onCompleted() { } }); - awaitForComplete(cdl, s); + BlockingUtils.awaitForComplete(cdl, s); Throwable e = error[0]; if (e != null) { if (e instanceof RuntimeException) { diff --git a/src/main/java/rx/singles/BlockingSingle.java b/src/main/java/rx/singles/BlockingSingle.java new file mode 100644 index 0000000000..6821bc5b82 --- /dev/null +++ b/src/main/java/rx/singles/BlockingSingle.java @@ -0,0 +1,106 @@ +/** + * Copyright 2015 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.singles; + +import rx.Single; +import rx.SingleSubscriber; +import rx.Subscription; +import rx.annotations.Experimental; +import rx.internal.operators.BlockingOperatorToFuture; +import rx.internal.util.BlockingUtils; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +/** + * {@code BlockingSingle} is a blocking "version" of {@link Single} that provides blocking + * operators. + *

+ * You construct a {@code BlockingSingle} from a {@code Single} with {@link #from(Single)} + * or {@link Single#toBlocking()}. + */ +@Experimental +public class BlockingSingle { + private final Single single; + + private BlockingSingle(Single single) { + this.single = single; + } + + /** + * Converts a {@link Single} into a {@code BlockingSingle}. + * + * @param single the {@link Single} you want to convert + * @return a {@code BlockingSingle} version of {@code single} + */ + @Experimental + public static BlockingSingle from(Single single) { + return new BlockingSingle(single); + } + + /** + * Returns the item emitted by this {@code BlockingSingle}. + *

+ * If the underlying {@link Single} returns successfully, the value emitted + * by the {@link Single} is returned. If the {@link Single} emits an error, + * the throwable emitted ({@link SingleSubscriber#onError(Throwable)}) is + * thrown. + * + * @return the value emitted by this {@code BlockingSingle} + */ + @Experimental + public T value() { + final AtomicReference returnItem = new AtomicReference(); + final AtomicReference returnException = new AtomicReference(); + final CountDownLatch latch = new CountDownLatch(1); + Subscription subscription = single.subscribe(new SingleSubscriber() { + @Override + public void onSuccess(T value) { + returnItem.set(value); + latch.countDown(); + } + + @Override + public void onError(Throwable error) { + returnException.set(error); + latch.countDown(); + } + }); + + BlockingUtils.awaitForComplete(latch, subscription); + Throwable throwable = returnException.get(); + if (throwable != null) { + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + throw new RuntimeException(throwable); + } + return returnItem.get(); + } + + /** + * Returns a {@link Future} representing the value emitted by this {@code BlockingSingle}. + * + * @return a {@link Future} that returns the value + */ + @Experimental + public Future toFuture() { + return BlockingOperatorToFuture.toFuture(single.toObservable()); + } +} + diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index bba4d09bc7..5bc24a6368 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -13,6 +13,7 @@ package rx; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -40,6 +41,7 @@ import rx.functions.Func1; import rx.functions.Func2; import rx.schedulers.TestScheduler; +import rx.singles.BlockingSingle; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; @@ -260,6 +262,14 @@ public void call(SingleSubscriber s) { ts.assertValue("hello"); } + @Test + public void testToBlocking() { + Single s = Single.just("one"); + BlockingSingle blocking = s.toBlocking(); + assertNotNull(blocking); + assertEquals("one", blocking.value()); + } + @Test public void testUnsubscribe() throws InterruptedException { TestSubscriber ts = new TestSubscriber(); diff --git a/src/test/java/rx/internal/util/BlockingUtilsTest.java b/src/test/java/rx/internal/util/BlockingUtilsTest.java new file mode 100644 index 0000000000..ff430c7aee --- /dev/null +++ b/src/test/java/rx/internal/util/BlockingUtilsTest.java @@ -0,0 +1,105 @@ +/** + * Copyright 2015 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 static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import rx.Observable; +import rx.Subscriber; +import rx.Subscription; +import rx.schedulers.Schedulers; + +/** + * Test suite for {@link BlockingUtils}. + */ +public class BlockingUtilsTest { + @Test + public void awaitCompleteShouldReturnIfCountIsZero() { + Subscription subscription = mock(Subscription.class); + CountDownLatch latch = new CountDownLatch(0); + BlockingUtils.awaitForComplete(latch, subscription); + verifyZeroInteractions(subscription); + } + + @Test + public void awaitCompleteShouldReturnOnEmpty() { + final CountDownLatch latch = new CountDownLatch(1); + Subscriber subscription = createSubscription(latch); + Observable observable = Observable.empty().subscribeOn(Schedulers.newThread()); + observable.subscribe(subscription); + BlockingUtils.awaitForComplete(latch, subscription); + } + + @Test + public void awaitCompleteShouldReturnOnError() { + final CountDownLatch latch = new CountDownLatch(1); + Subscriber subscription = createSubscription(latch); + Observable observable = Observable.error(new RuntimeException()).subscribeOn(Schedulers.newThread()); + observable.subscribe(subscription); + BlockingUtils.awaitForComplete(latch, subscription); + } + + @Test + public void shouldThrowRuntimeExceptionOnThreadInterrupted() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final Subscription subscription = mock(Subscription.class); + final AtomicReference caught = new AtomicReference(); + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + Thread.currentThread().interrupt(); + try { + BlockingUtils.awaitForComplete(latch, subscription); + } catch (RuntimeException e) { + caught.set(e); + } + } + }); + thread.run(); + verify(subscription).unsubscribe(); + Exception actual = caught.get(); + assertNotNull(actual); + assertNotNull(actual.getCause()); + assertTrue(actual.getCause() instanceof InterruptedException); + } + + + private static Subscriber createSubscription(final CountDownLatch latch) { + return new Subscriber() { + @Override + public void onNext(T t) { + //no-oop + } + + @Override + public void onError(Throwable e) { + latch.countDown(); + } + + @Override + public void onCompleted() { + latch.countDown(); + } + }; + } +} diff --git a/src/test/java/rx/singles/BlockingSingleTest.java b/src/test/java/rx/singles/BlockingSingleTest.java new file mode 100644 index 0000000000..48c5b7eb03 --- /dev/null +++ b/src/test/java/rx/singles/BlockingSingleTest.java @@ -0,0 +1,80 @@ +/** + * Copyright 2015 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.singles; + +import static org.junit.Assert.*; + +import java.util.concurrent.Future; + +import org.junit.Test; + +import rx.Single; +import rx.exceptions.TestException; + +/** + * Test suite for {@link BlockingSingle}. + */ +public class BlockingSingleTest { + + @Test + public void testSingleGet() { + Single single = Single.just("one"); + BlockingSingle blockingSingle = BlockingSingle.from(single); + assertEquals("one", blockingSingle.value()); + } + + @Test + public void testSingleError() { + TestException expected = new TestException(); + Single single = Single.error(expected); + BlockingSingle blockingSingle = BlockingSingle.from(single); + + try { + blockingSingle.value(); + fail("Expecting an exception to be thrown"); + } catch (Exception caughtException) { + assertSame(expected, caughtException); + } + } + + @Test + public void testSingleErrorChecked() { + TestCheckedException expected = new TestCheckedException(); + Single single = Single.error(expected); + BlockingSingle blockingSingle = BlockingSingle.from(single); + + try { + blockingSingle.value(); + fail("Expecting an exception to be thrown"); + } catch (Exception caughtException) { + assertNotNull(caughtException.getCause()); + assertSame(expected, caughtException.getCause() ); + } + } + + @Test + public void testSingleToFuture() throws Exception { + Single single = Single.just("one"); + BlockingSingle blockingSingle = BlockingSingle.from(single); + Future future = blockingSingle.toFuture(); + String result = future.get(); + assertEquals("one", result); + } + + private static final class TestCheckedException extends Exception { + } +} From d01cd061e378494d342e6dd03d469ff31a42184a Mon Sep 17 00:00:00 2001 From: Mark Rietveld Date: Mon, 2 Nov 2015 17:57:07 -0800 Subject: [PATCH 442/857] 1.x Remove all instances of Atomic*FieldUpdater Replace them all with their respective Atomic* counterparts For example AtomicLongFieldUpdater -> AtomicLong Addresses https://github.com/ReactiveX/RxJava/issues/3459 --- .../operators/BlockingOperatorLatest.java | 12 +- .../operators/BlockingOperatorNext.java | 12 +- .../operators/BufferUntilSubscriber.java | 28 ++-- .../operators/OnSubscribeCombineLatest.java | 11 +- .../rx/internal/operators/OperatorConcat.java | 35 ++--- .../operators/OperatorMaterialize.java | 17 +- .../internal/operators/OperatorObserveOn.java | 38 ++--- .../operators/OperatorRetryWithPredicate.java | 11 +- .../operators/OperatorSampleWithTime.java | 12 +- .../operators/OperatorTimeoutBase.java | 27 ++-- .../rx/internal/operators/OperatorZip.java | 15 +- .../operators/TakeLastQueueProducer.java | 26 ++-- .../util/BackpressureDrainManager.java | 19 +-- .../rx/internal/util/PaddedAtomicInteger.java | 30 ---- .../util/PaddedAtomicIntegerBase.java | 84 ---------- .../rx/internal/util/RxThreadFactory.java | 9 +- .../util/SubscriptionIndexedRingBuffer.java | 145 ------------------ .../rx/schedulers/TrampolineScheduler.java | 7 +- src/main/java/rx/subjects/AsyncSubject.java | 14 +- .../java/rx/subjects/BehaviorSubject.java | 22 +-- src/main/java/rx/subjects/PublishSubject.java | 8 +- src/main/java/rx/subjects/ReplaySubject.java | 45 +++--- .../subjects/SubjectSubscriptionManager.java | 37 ++--- src/main/java/rx/subjects/TestSubject.java | 2 +- .../rx/subscriptions/BooleanSubscription.java | 26 ++-- .../MultipleAssignmentSubscription.java | 20 +-- .../subscriptions/RefCountSubscription.java | 34 ++-- .../rx/subscriptions/SerialSubscription.java | 20 +-- 28 files changed, 223 insertions(+), 543 deletions(-) delete mode 100644 src/main/java/rx/internal/util/PaddedAtomicInteger.java delete mode 100644 src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java delete mode 100644 src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java diff --git a/src/main/java/rx/internal/operators/BlockingOperatorLatest.java b/src/main/java/rx/internal/operators/BlockingOperatorLatest.java index c5f90f3828..5b2b798995 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorLatest.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorLatest.java @@ -18,7 +18,7 @@ import java.util.Iterator; import java.util.NoSuchElementException; import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; import rx.Notification; import rx.Observable; @@ -59,15 +59,11 @@ public Iterator iterator() { static final class LatestObserverIterator extends Subscriber> implements Iterator { final Semaphore notify = new Semaphore(0); // observer's notification - volatile Notification value; - /** Updater for the value field. */ - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater REFERENCE_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(LatestObserverIterator.class, Notification.class, "value"); + final AtomicReference> value = new AtomicReference>(); @Override public void onNext(Notification args) { - boolean wasntAvailable = REFERENCE_UPDATER.getAndSet(this, args) == null; + boolean wasntAvailable = value.getAndSet(args) == null; if (wasntAvailable) { notify.release(); } @@ -103,7 +99,7 @@ public boolean hasNext() { } @SuppressWarnings("unchecked") - Notification n = REFERENCE_UPDATER.getAndSet(this, null); + Notification n = value.getAndSet(null); iNotif = n; if (iNotif.isOnError()) { throw Exceptions.propagate(iNotif.getThrowable()); diff --git a/src/main/java/rx/internal/operators/BlockingOperatorNext.java b/src/main/java/rx/internal/operators/BlockingOperatorNext.java index 05b5b8f1d8..abfab09f2c 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorNext.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorNext.java @@ -19,7 +19,7 @@ import java.util.NoSuchElementException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicInteger; import rx.Notification; import rx.Observable; @@ -147,11 +147,7 @@ public void remove() { private static class NextObserver extends Subscriber> { private final BlockingQueue> buf = new ArrayBlockingQueue>(1); - @SuppressWarnings("unused") - volatile int waiting; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WAITING_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(NextObserver.class, "waiting"); + final AtomicInteger waiting = new AtomicInteger(); @Override public void onCompleted() { @@ -166,7 +162,7 @@ public void onError(Throwable e) { @Override public void onNext(Notification args) { - if (WAITING_UPDATER.getAndSet(this, 0) == 1 || !args.isOnNext()) { + if (waiting.getAndSet(0) == 1 || !args.isOnNext()) { Notification toOffer = args; while (!buf.offer(toOffer)) { Notification concurrentItem = buf.poll(); @@ -185,7 +181,7 @@ public Notification takeNext() throws InterruptedException { return buf.take(); } void setWaiting(int value) { - waiting = value; + waiting.set(value); } } } diff --git a/src/main/java/rx/internal/operators/BufferUntilSubscriber.java b/src/main/java/rx/internal/operators/BufferUntilSubscriber.java index e4722c9a60..f486c397f7 100644 --- a/src/main/java/rx/internal/operators/BufferUntilSubscriber.java +++ b/src/main/java/rx/internal/operators/BufferUntilSubscriber.java @@ -16,7 +16,7 @@ package rx.internal.operators; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; import rx.Observer; import rx.Subscriber; @@ -59,15 +59,9 @@ public static BufferUntilSubscriber create() { } /** The common state. */ - static final class State { - volatile Observer observerRef = null; - /** Field updater for observerRef. */ - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater OBSERVER_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(State.class, Observer.class, "observerRef"); - + static final class State extends AtomicReference> { boolean casObserverRef(Observer expected, Observer next) { - return OBSERVER_UPDATER.compareAndSet(this, expected, next); + return compareAndSet(expected, next); } final Object guard = new Object(); @@ -92,7 +86,7 @@ public void call(final Subscriber s) { @SuppressWarnings("unchecked") @Override public void call() { - state.observerRef = EMPTY_OBSERVER; + state.set(EMPTY_OBSERVER); } })); boolean win = false; @@ -107,7 +101,7 @@ public void call() { while(true) { Object o; while ((o = state.buffer.poll()) != null) { - nl.accept(state.observerRef, o); + nl.accept(state.get(), o); } synchronized (state.guard) { if (state.buffer.isEmpty()) { @@ -138,7 +132,7 @@ private BufferUntilSubscriber(State state) { private void emit(Object v) { synchronized (state.guard) { state.buffer.add(v); - if (state.observerRef != null && !state.emitting) { + if (state.get() != null && !state.emitting) { // Have an observer and nobody is emitting, // should drain the `buffer` forward = true; @@ -148,7 +142,7 @@ private void emit(Object v) { if (forward) { Object o; while ((o = state.buffer.poll()) != null) { - state.nl.accept(state.observerRef, o); + state.nl.accept(state.get(), o); } // Because `emit(Object v)` will be called in sequence, // no event will be put into `buffer` after we drain it. @@ -158,7 +152,7 @@ private void emit(Object v) { @Override public void onCompleted() { if (forward) { - state.observerRef.onCompleted(); + state.get().onCompleted(); } else { emit(state.nl.completed()); @@ -168,7 +162,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { if (forward) { - state.observerRef.onError(e); + state.get().onError(e); } else { emit(state.nl.error(e)); @@ -178,7 +172,7 @@ public void onError(Throwable e) { @Override public void onNext(T t) { if (forward) { - state.observerRef.onNext(t); + state.get().onNext(t); } else { emit(state.nl.next(t)); @@ -188,7 +182,7 @@ public void onNext(T t) { @Override public boolean hasObservers() { synchronized (state.guard) { - return state.observerRef != null; + return state.get() != null; } } diff --git a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java index 54e1335205..5df99b2585 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; import rx.Observable; import rx.Observable.OnSubscribe; @@ -90,10 +89,7 @@ final static class MultiSourceProducer implements Producer { private final BitSet completion; private volatile int completionCount; // does this need to be volatile or is WIP sufficient? - @SuppressWarnings("unused") - private volatile long counter; - @SuppressWarnings("rawtypes") - private static final AtomicLongFieldUpdater WIP = AtomicLongFieldUpdater.newUpdater(MultiSourceProducer.class, "counter"); + private final AtomicLong counter = new AtomicLong(); @SuppressWarnings("unchecked") public MultiSourceProducer(final Subscriber child, final List> sources, FuncN combinator) { @@ -139,7 +135,8 @@ public void request(long n) { * that there is always once who acts on each `tick`. Same concept as used in OperationObserveOn. */ void tick() { - if (WIP.getAndIncrement(this) == 0) { + AtomicLong localCounter = this.counter; + if (localCounter.getAndIncrement() == 0) { int emitted = 0; do { // we only emit if requested > 0 @@ -155,7 +152,7 @@ void tick() { } } } - } while (WIP.decrementAndGet(this) > 0); + } while (localCounter.decrementAndGet() > 0); if (emitted > 0) { for (MultiSourceRequestableSubscriber s : subscribers) { s.requestUpTo(emitted); diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index e91e669bba..398cbacf4d 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -16,8 +16,8 @@ package rx.internal.operators; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import rx.Observable; import rx.Observable.Operator; @@ -84,14 +84,10 @@ static final class ConcatSubscriber extends Subscriber currentSubscriber; - volatile int wip; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP = AtomicIntegerFieldUpdater.newUpdater(ConcatSubscriber.class, "wip"); + final AtomicInteger wip = new AtomicInteger(); // accessed by REQUESTED - private volatile long requested; - @SuppressWarnings("rawtypes") - private static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(ConcatSubscriber.class, "requested"); + private final AtomicLong requested = new AtomicLong(); private final ProducerArbiter arbiter; public ConcatSubscriber(Subscriber s, SerialSubscription current) { @@ -118,10 +114,10 @@ public void onStart() { private void requestFromChild(long n) { if (n <=0) return; // we track 'requested' so we know whether we should subscribe the next or not - long previous = BackpressureUtils.getAndAddRequest(REQUESTED, this, n); + long previous = BackpressureUtils.getAndAddRequest(requested, n); arbiter.request(n); if (previous == 0) { - if (currentSubscriber == null && wip > 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(); @@ -130,13 +126,13 @@ private void requestFromChild(long n) { } private void decrementRequested() { - REQUESTED.decrementAndGet(this); + requested.decrementAndGet(); } @Override public void onNext(Observable t) { queue.add(nl.next(t)); - if (WIP.getAndIncrement(this) == 0) { + if (wip.getAndIncrement() == 0) { subscribeNext(); } } @@ -150,7 +146,7 @@ public void onError(Throwable e) { @Override public void onCompleted() { queue.add(nl.completed()); - if (WIP.getAndIncrement(this) == 0) { + if (wip.getAndIncrement() == 0) { subscribeNext(); } } @@ -158,14 +154,14 @@ public void onCompleted() { void completeInner() { currentSubscriber = null; - if (WIP.decrementAndGet(this) > 0) { + if (wip.decrementAndGet() > 0) { subscribeNext(); } request(1); } void subscribeNext() { - if (requested > 0) { + if (requested.get() > 0) { Object o = queue.poll(); if (nl.isCompleted(o)) { child.onCompleted(); @@ -189,10 +185,7 @@ static class ConcatInnerSubscriber extends Subscriber { private final Subscriber child; private final ConcatSubscriber parent; - @SuppressWarnings("unused") - private volatile int once = 0; - @SuppressWarnings("rawtypes") - private final static AtomicIntegerFieldUpdater ONCE = AtomicIntegerFieldUpdater.newUpdater(ConcatInnerSubscriber.class, "once"); + private final AtomicInteger once = new AtomicInteger(); private final ProducerArbiter arbiter; public ConcatInnerSubscriber(ConcatSubscriber parent, Subscriber child, ProducerArbiter arbiter) { @@ -210,7 +203,7 @@ public void onNext(T t) { @Override public void onError(Throwable e) { - if (ONCE.compareAndSet(this, 0, 1)) { + if (once.compareAndSet(0, 1)) { // terminal error through parent so everything gets cleaned up, including this inner parent.onError(e); } @@ -218,7 +211,7 @@ public void onError(Throwable e) { @Override public void onCompleted() { - if (ONCE.compareAndSet(this, 0, 1)) { + if (once.compareAndSet(0, 1)) { // terminal completion to parent so it continues to the next parent.completeInner(); } diff --git a/src/main/java/rx/internal/operators/OperatorMaterialize.java b/src/main/java/rx/internal/operators/OperatorMaterialize.java index e074cd5816..32b49c6c77 100644 --- a/src/main/java/rx/internal/operators/OperatorMaterialize.java +++ b/src/main/java/rx/internal/operators/OperatorMaterialize.java @@ -15,7 +15,7 @@ */ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; import rx.Notification; import rx.Observable.Operator; @@ -76,10 +76,7 @@ private static class ParentSubscriber extends Subscriber { // guarded by this private boolean missed = false; - private volatile long requested; - @SuppressWarnings("rawtypes") - private static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater - .newUpdater(ParentSubscriber.class, "requested"); + private final AtomicLong requested = new AtomicLong(); ParentSubscriber(Subscriber> child) { this.child = child; @@ -91,7 +88,7 @@ public void onStart() { } void requestMore(long n) { - BackpressureUtils.getAndAddRequest(REQUESTED, this, n); + BackpressureUtils.getAndAddRequest(requested, n); request(n); drain(); } @@ -117,12 +114,13 @@ public void onNext(T t) { private void decrementRequested() { // atomically decrement requested + AtomicLong localRequested = this.requested; while (true) { - long r = requested; + long r = localRequested.get(); if (r == Long.MAX_VALUE) { // don't decrement if unlimited requested return; - } else if (REQUESTED.compareAndSet(this, r, r - 1)) { + } else if (localRequested.compareAndSet(r, r - 1)) { return; } } @@ -137,11 +135,12 @@ private void drain() { } } // drain loop + final AtomicLong localRequested = this.requested; while (!child.isUnsubscribed()) { Notification tn; tn = terminalNotification; if (tn != null) { - if (requested > 0) { + if (localRequested.get() > 0) { // allow tn to be GC'd after the onNext call terminalNotification = null; // emit the terminal notification diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 1f1f380ff0..8aff74e67f 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -16,8 +16,8 @@ package rx.internal.operators; import java.util.Queue; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import rx.Observable.Operator; import rx.Producer; @@ -79,18 +79,10 @@ private static final class ObserveOnSubscriber extends Subscriber { // the status of the current stream volatile boolean finished = false; - @SuppressWarnings("unused") - volatile long requested = 0; + final AtomicLong requested = new AtomicLong(); - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "requested"); - - @SuppressWarnings("unused") - volatile long counter; + final AtomicLong counter = new AtomicLong(); - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater COUNTER_UPDATER = AtomicLongFieldUpdater.newUpdater(ObserveOnSubscriber.class, "counter"); - volatile Throwable error; // do NOT pass the Subscriber through to couple the subscription chain ... unsubscribing on the parent should @@ -114,7 +106,7 @@ void init() { @Override public void request(long n) { - BackpressureUtils.getAndAddRequest(REQUESTED, ObserveOnSubscriber.this, n); + BackpressureUtils.getAndAddRequest(requested, n); schedule(); } @@ -173,7 +165,7 @@ public void call() { }; protected void schedule() { - if (COUNTER_UPDATER.getAndIncrement(this) == 0) { + if (counter.getAndIncrement() == 0) { recursiveScheduler.schedule(action); } } @@ -181,10 +173,12 @@ protected void schedule() { // only execute this from schedule() void pollQueue() { int emitted = 0; + final AtomicLong localRequested = this.requested; + final AtomicLong localCounter = this.counter; do { - counter = 1; + localCounter.set(1); long produced = 0; - long r = requested; + long r = localRequested.get(); for (;;) { if (child.isUnsubscribed()) return; @@ -216,20 +210,18 @@ void pollQueue() { break; } } - if (produced > 0 && requested != Long.MAX_VALUE) { - REQUESTED.addAndGet(this, -produced); + if (produced > 0 && localRequested.get() != Long.MAX_VALUE) { + localRequested.addAndGet(-produced); } - } while (COUNTER_UPDATER.decrementAndGet(this) > 0); + } while (localCounter.decrementAndGet() > 0); if (emitted > 0) { request(emitted); } } } - static final class ScheduledUnsubscribe implements Subscription { + static final class ScheduledUnsubscribe extends AtomicInteger implements Subscription { final Scheduler.Worker worker; - volatile int once; - static final AtomicIntegerFieldUpdater ONCE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(ScheduledUnsubscribe.class, "once"); volatile boolean unsubscribed = false; public ScheduledUnsubscribe(Scheduler.Worker worker) { @@ -243,7 +235,7 @@ public boolean isUnsubscribed() { @Override public void unsubscribe() { - if (ONCE_UPDATER.getAndSet(this, 1) == 0) { + if (getAndSet(1) == 0) { worker.schedule(new Action0() { @Override public void call() { diff --git a/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java b/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java index bdfcd3dbeb..0e5111b6c4 100644 --- a/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorRetryWithPredicate.java @@ -15,7 +15,7 @@ */ package rx.internal.operators; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicInteger; import rx.Observable; import rx.Producer; @@ -53,10 +53,7 @@ static final class SourceSubscriber extends Subscriber> { final SerialSubscription serialSubscription; final ProducerArbiter pa; - volatile int attempts; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ATTEMPTS_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(SourceSubscriber.class, "attempts"); + final AtomicInteger attempts = new AtomicInteger(); public SourceSubscriber(Subscriber child, final Func2 predicate, @@ -88,7 +85,7 @@ public void onNext(final Observable o) { @Override public void call() { final Action0 _self = this; - ATTEMPTS_UPDATER.incrementAndGet(SourceSubscriber.this); + attempts.incrementAndGet(); // new subscription each time so if it unsubscribes itself it does not prevent retries // by unsubscribing the child subscription @@ -106,7 +103,7 @@ public void onCompleted() { public void onError(Throwable e) { if (!done) { done = true; - if (predicate.call(attempts, e) && !inner.isUnsubscribed()) { + if (predicate.call(attempts.get(), e) && !inner.isUnsubscribed()) { // retry again inner.schedule(_self); } else { diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java index f3130cbb97..0fdcbd2c68 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithTime.java @@ -16,7 +16,8 @@ package rx.internal.operators; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; + import rx.Observable.Operator; import rx.Scheduler; import rx.Scheduler.Worker; @@ -64,11 +65,8 @@ static final class SamplerSubscriber extends Subscriber implements Action0 /** Indicates that no value is available. */ private static final Object EMPTY_TOKEN = new Object(); /** The shared value between the observer and the timed action. */ - volatile Object value = EMPTY_TOKEN; + final AtomicReference value = new AtomicReference(EMPTY_TOKEN); /** Updater for the value field. */ - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater VALUE_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(SamplerSubscriber.class, Object.class, "value"); public SamplerSubscriber(Subscriber subscriber) { this.subscriber = subscriber; } @@ -80,7 +78,7 @@ public void onStart() { @Override public void onNext(T t) { - value = t; + value.set(t); } @Override @@ -97,7 +95,7 @@ public void onCompleted() { @Override public void call() { - Object localValue = VALUE_UPDATER.getAndSet(this, EMPTY_TOKEN); + Object localValue = value.getAndSet(EMPTY_TOKEN); if (localValue != EMPTY_TOKEN) { try { @SuppressWarnings("unchecked") diff --git a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java index 038bf88a0c..65b940640c 100644 --- a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java +++ b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java @@ -16,8 +16,8 @@ package rx.internal.operators; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import rx.Observable; import rx.Observable.Operator; @@ -90,16 +90,9 @@ public Subscriber call(Subscriber subscriber) { private final Observable other; private final Scheduler.Worker inner; - volatile int terminated; - volatile long actual; + final AtomicInteger terminated = new AtomicInteger(); + final AtomicLong actual = new AtomicLong(); - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater TERMINATED_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(TimeoutSubscriber.class, "terminated"); - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater ACTUAL_UPDATER - = AtomicLongFieldUpdater.newUpdater(TimeoutSubscriber.class, "actual"); - private TimeoutSubscriber( SerializedSubscriber serializedSubscriber, TimeoutStub timeoutStub, SerialSubscription serial, @@ -117,14 +110,14 @@ private TimeoutSubscriber( public void onNext(T value) { boolean onNextWins = false; synchronized (gate) { - if (terminated == 0) { - ACTUAL_UPDATER.incrementAndGet(this); + if (terminated.get() == 0) { + actual.incrementAndGet(); onNextWins = true; } } if (onNextWins) { serializedSubscriber.onNext(value); - serial.set(timeoutStub.call(this, actual, value, inner)); + serial.set(timeoutStub.call(this, actual.get(), value, inner)); } } @@ -132,7 +125,7 @@ public void onNext(T value) { public void onError(Throwable error) { boolean onErrorWins = false; synchronized (gate) { - if (TERMINATED_UPDATER.getAndSet(this, 1) == 0) { + if (terminated.getAndSet(1) == 0) { onErrorWins = true; } } @@ -146,7 +139,7 @@ public void onError(Throwable error) { public void onCompleted() { boolean onCompletedWins = false; synchronized (gate) { - if (TERMINATED_UPDATER.getAndSet(this, 1) == 0) { + if (terminated.getAndSet(1) == 0) { onCompletedWins = true; } } @@ -160,7 +153,7 @@ public void onTimeout(long seqId) { long expected = seqId; boolean timeoutWins = false; synchronized (gate) { - if (expected == actual && TERMINATED_UPDATER.getAndSet(this, 1) == 0) { + if (expected == actual.get() && terminated.getAndSet(1) == 0) { timeoutWins = true; } } diff --git a/src/main/java/rx/internal/operators/OperatorZip.java b/src/main/java/rx/internal/operators/OperatorZip.java index d4f0560718..df9dc4a00d 100644 --- a/src/main/java/rx/internal/operators/OperatorZip.java +++ b/src/main/java/rx/internal/operators/OperatorZip.java @@ -16,14 +16,14 @@ package rx.internal.operators; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; import rx.Observable; import rx.Observable.Operator; -import rx.exceptions.*; import rx.Observer; import rx.Producer; import rx.Subscriber; +import rx.exceptions.Exceptions; +import rx.exceptions.MissingBackpressureException; import rx.functions.Func2; import rx.functions.Func3; import rx.functions.Func4; @@ -175,16 +175,11 @@ public void request(long n) { } - private static final class Zip { + private static final class Zip extends AtomicLong { private final Observer child; private final FuncN zipFunction; private final CompositeSubscription childSubscription = new CompositeSubscription(); - @SuppressWarnings("unused") - volatile long counter; - @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater COUNTER_UPDATER = AtomicLongFieldUpdater.newUpdater(Zip.class, "counter"); - static final int THRESHOLD = (int) (RxRingBuffer.SIZE * 0.7); int emitted = 0; // not volatile/synchronized as accessed inside COUNTER_UPDATER block @@ -227,7 +222,7 @@ void tick() { // nothing yet to do (initial request from Producer) return; } - if (COUNTER_UPDATER.getAndIncrement(this) == 0) { + if (getAndIncrement() == 0) { final int length = observers.length; final Observer child = this.child; final AtomicLong requested = this.requested; @@ -290,7 +285,7 @@ void tick() { break; } } - } while (COUNTER_UPDATER.decrementAndGet(this) > 0); + } while (decrementAndGet() > 0); } } diff --git a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java index 7fc5ce9235..664dfd0e3a 100644 --- a/src/main/java/rx/internal/operators/TakeLastQueueProducer.java +++ b/src/main/java/rx/internal/operators/TakeLastQueueProducer.java @@ -16,14 +16,14 @@ package rx.internal.operators; +import java.util.Deque; +import java.util.concurrent.atomic.AtomicLong; + import rx.Producer; import rx.Subscriber; import rx.exceptions.Exceptions; -import java.util.Deque; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -final class TakeLastQueueProducer implements Producer { +final class TakeLastQueueProducer extends AtomicLong implements Producer { private final NotificationLite notification; private final Deque deque; @@ -36,10 +36,6 @@ public TakeLastQueueProducer(NotificationLite n, Deque q, Subscriber< this.subscriber = subscriber; } - private volatile long requested = 0; - @SuppressWarnings("rawtypes") - private static final AtomicLongFieldUpdater REQUESTED_UPDATER = AtomicLongFieldUpdater.newUpdater(TakeLastQueueProducer.class, "requested"); - void startEmitting() { if (!emittingStarted) { emittingStarted = true; @@ -49,14 +45,14 @@ void startEmitting() { @Override public void request(long n) { - if (requested == Long.MAX_VALUE) { + if (get() == Long.MAX_VALUE) { return; } long _c; if (n == Long.MAX_VALUE) { - _c = REQUESTED_UPDATER.getAndSet(this, Long.MAX_VALUE); + _c = getAndSet(Long.MAX_VALUE); } else { - _c = BackpressureUtils.getAndAddRequest(REQUESTED_UPDATER, this, n); + _c = BackpressureUtils.getAndAddRequest(this, n); } if (!emittingStarted) { // we haven't started yet, so record what was requested and return @@ -66,7 +62,7 @@ public void request(long n) { } void emit(long previousRequested) { - if (requested == Long.MAX_VALUE) { + if (get() == Long.MAX_VALUE) { // fast-path without backpressure if (previousRequested == 0) { try { @@ -91,7 +87,7 @@ void emit(long previousRequested) { * 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 = requested; + long numToEmit = get(); int emitted = 0; Object o; while (--numToEmit >= 0 && (o = deque.poll()) != null) { @@ -106,14 +102,14 @@ void emit(long previousRequested) { } } for (; ; ) { - long oldRequested = requested; + 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 (REQUESTED_UPDATER.compareAndSet(this, oldRequested, newRequested)) { + if (compareAndSet(oldRequested, newRequested)) { if (newRequested == 0) { // we're done emitting the number requested so return return; diff --git a/src/main/java/rx/internal/util/BackpressureDrainManager.java b/src/main/java/rx/internal/util/BackpressureDrainManager.java index f4a95573e7..38f714b67f 100644 --- a/src/main/java/rx/internal/util/BackpressureDrainManager.java +++ b/src/main/java/rx/internal/util/BackpressureDrainManager.java @@ -15,7 +15,7 @@ */ package rx.internal.util; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; import rx.Producer; import rx.annotations.Experimental; @@ -26,7 +26,7 @@ * terminal events. */ @Experimental -public final class BackpressureDrainManager implements Producer { +public final class BackpressureDrainManager extends AtomicLong implements Producer { /** * Interface representing the minimal callbacks required * to operate the drain part of a backpressure system. @@ -61,11 +61,6 @@ public interface BackpressureQueueCallback { void complete(Throwable exception); } - /** The request counter, updated via REQUESTED_COUNTER. */ - protected volatile long requestedCount; - /** Atomically updates the the requestedCount field. */ - protected static final AtomicLongFieldUpdater REQUESTED_COUNT - = AtomicLongFieldUpdater.newUpdater(BackpressureDrainManager.class, "requestedCount"); /** Indicates if one is in emitting phase, guarded by this. */ protected boolean emitting; /** Indicates a terminal state. */ @@ -138,7 +133,7 @@ public final void request(long n) { long r; long u; do { - r = requestedCount; + r = get(); mayDrain = r == 0; if (r == Long.MAX_VALUE) { break; @@ -153,7 +148,7 @@ public final void request(long n) { u = r + n; } } - } while (!REQUESTED_COUNT.compareAndSet(this, r, u)); + } while (!compareAndSet(r, u)); // since we implement producer, we have to call drain // on a 0-n request transition if (mayDrain) { @@ -174,7 +169,7 @@ public final void drain() { emitting = true; term = terminated; } - n = requestedCount; + n = get(); boolean skipFinal = false; try { BackpressureQueueCallback a = actual; @@ -210,7 +205,7 @@ public final void drain() { term = terminated; boolean more = a.peek() != null; // if no backpressure below - if (requestedCount == Long.MAX_VALUE) { + if (get() == Long.MAX_VALUE) { // no new data arrived since the last poll if (!more && !term) { skipFinal = true; @@ -219,7 +214,7 @@ public final void drain() { } n = Long.MAX_VALUE; } else { - n = REQUESTED_COUNT.addAndGet(this, -emitted); + n = addAndGet(-emitted); if ((n == 0 || !more) && (!term || more)) { skipFinal = true; emitting = false; diff --git a/src/main/java/rx/internal/util/PaddedAtomicInteger.java b/src/main/java/rx/internal/util/PaddedAtomicInteger.java deleted file mode 100644 index e0ebdd3a21..0000000000 --- a/src/main/java/rx/internal/util/PaddedAtomicInteger.java +++ /dev/null @@ -1,30 +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.util; - -/** - * A padded atomic integer to fill in 4 cache lines to avoid any false sharing or - * adjacent prefetch. - * Based on Netty's implementation. - */ -public final class PaddedAtomicInteger extends PaddedAtomicIntegerBase { - /** */ - private static final long serialVersionUID = 8781891581317286855L; - /** Padding. */ - public transient long p16, p17, p18, p19, p20, p21, p22; // 56 bytes (the remaining 8 is in the base) - /** Padding. */ - public transient long p24, p25, p26, p27, p28, p29, p30, p31; // 64 bytes -} diff --git a/src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java b/src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java deleted file mode 100644 index afa67e4b81..0000000000 --- a/src/main/java/rx/internal/util/PaddedAtomicIntegerBase.java +++ /dev/null @@ -1,84 +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.util; - -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -/** - * The atomic integer base padded at the front. - * Based on Netty's implementation. - */ -abstract class PaddedAtomicIntegerBase extends FrontPadding { - - private static final long serialVersionUID = 6513142711280243198L; - - private static final AtomicIntegerFieldUpdater updater; - - static { - updater = AtomicIntegerFieldUpdater.newUpdater(PaddedAtomicIntegerBase.class, "value"); - } - - private volatile int value; // 8-byte object field (or 4-byte + padding) - - public final int get() { - return value; - } - - public final void set(int newValue) { - this.value = newValue; - } - - public final void lazySet(int newValue) { - updater.lazySet(this, newValue); - } - - public final boolean compareAndSet(int expect, int update) { - return updater.compareAndSet(this, expect, update); - } - - public final boolean weakCompareAndSet(int expect, int update) { - return updater.weakCompareAndSet(this, expect, update); - } - - public final int getAndSet(int newValue) { - return updater.getAndSet(this, value); - } - - public final int getAndAdd(int delta) { - return updater.getAndAdd(this, delta); - } - public final int incrementAndGet() { - return updater.incrementAndGet(this); - } - public final int decrementAndGet() { - return updater.decrementAndGet(this); - } - public final int getAndIncrement() { - return updater.getAndIncrement(this); - } - public final int getAndDecrement() { - return updater.getAndDecrement(this); - } - public final int addAndGet(int delta) { - return updater.addAndGet(this, delta); - } - - @Override - public String toString() { - return String.valueOf(get()); - } -} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/RxThreadFactory.java b/src/main/java/rx/internal/util/RxThreadFactory.java index 16f7551bcb..cc6d45d486 100644 --- a/src/main/java/rx/internal/util/RxThreadFactory.java +++ b/src/main/java/rx/internal/util/RxThreadFactory.java @@ -16,13 +16,10 @@ package rx.internal.util; import java.util.concurrent.ThreadFactory; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicLong; -public final class RxThreadFactory implements ThreadFactory { +public final class RxThreadFactory extends AtomicLong implements ThreadFactory { final String prefix; - volatile long counter; - static final AtomicLongFieldUpdater COUNTER_UPDATER - = AtomicLongFieldUpdater.newUpdater(RxThreadFactory.class, "counter"); public RxThreadFactory(String prefix) { this.prefix = prefix; @@ -30,7 +27,7 @@ public RxThreadFactory(String prefix) { @Override public Thread newThread(Runnable r) { - Thread t = new Thread(r, prefix + COUNTER_UPDATER.incrementAndGet(this)); + Thread t = new Thread(r, prefix + incrementAndGet()); t.setDaemon(true); return t; } diff --git a/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java b/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java deleted file mode 100644 index 6dcb2d566d..0000000000 --- a/src/main/java/rx/internal/util/SubscriptionIndexedRingBuffer.java +++ /dev/null @@ -1,145 +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.util; - -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; - -import rx.Subscription; -import rx.functions.Func1; - -/** - * Similar to CompositeSubscription but giving extra access to internals so we can reuse a datastructure. - *

- * NOTE: This purposefully is leaking the internal data structure through the API for efficiency reasons to avoid extra object allocations. - */ -public final class SubscriptionIndexedRingBuffer implements Subscription { - - private volatile IndexedRingBuffer subscriptions = IndexedRingBuffer.getInstance(); - private volatile int unsubscribed = 0; - @SuppressWarnings("rawtypes") - private final static AtomicIntegerFieldUpdater UNSUBSCRIBED = AtomicIntegerFieldUpdater.newUpdater(SubscriptionIndexedRingBuffer.class, "unsubscribed"); - - public SubscriptionIndexedRingBuffer() { - } - - @Override - public boolean isUnsubscribed() { - return unsubscribed == 1; - } - - /** - * Adds a new {@link Subscription} to this {@code CompositeSubscription} if the {@code CompositeSubscription} is not yet unsubscribed. If the {@code CompositeSubscription} is - * unsubscribed, {@code add} will indicate this by explicitly unsubscribing the new {@code Subscription} as - * well. - * - * @param s - * the {@link Subscription} to add - * - * @return int index that can be used to remove a Subscription - */ - public synchronized int add(final T s) { - // TODO figure out how to remove synchronized here. See https://github.com/ReactiveX/RxJava/issues/1420 - if (unsubscribed == 1 || subscriptions == null) { - s.unsubscribe(); - return -1; - } else { - int n = subscriptions.add(s); - // double check for race condition - if (unsubscribed == 1) { - s.unsubscribe(); - } - return n; - } - } - - /** - * Uses the Node received from `add` to remove this Subscription. - *

- * Unsubscribes the Subscription after removal - */ - public void remove(final int n) { - if (unsubscribed == 1 || subscriptions == null || n < 0) { - return; - } - Subscription t = subscriptions.remove(n); - if (t != null) { - // if we removed successfully we then need to call unsubscribe on it - if (t != null) { - t.unsubscribe(); - } - } - } - - /** - * Uses the Node received from `add` to remove this Subscription. - *

- * Does not unsubscribe the Subscription after removal. - */ - public void removeSilently(final int n) { - if (unsubscribed == 1 || subscriptions == null || n < 0) { - return; - } - subscriptions.remove(n); - } - - @Override - public void unsubscribe() { - if (UNSUBSCRIBED.compareAndSet(this, 0, 1) && subscriptions != null) { - // we will only get here once - unsubscribeFromAll(subscriptions); - - IndexedRingBuffer s = subscriptions; - subscriptions = null; - s.unsubscribe(); - } - } - - public int forEach(Func1 action) { - return forEach(action, 0); - } - - /** - * - * @param action - * @return int of last index seen if forEach exited early - */ - public synchronized int forEach(Func1 action, int startIndex) { - // TODO figure out how to remove synchronized here. See https://github.com/ReactiveX/RxJava/issues/1420 - if (unsubscribed == 1 || subscriptions == null) { - return 0; - } - return subscriptions.forEach(action, startIndex); - } - - private static void unsubscribeFromAll(IndexedRingBuffer subscriptions) { - if (subscriptions == null) { - return; - } - - // TODO migrate to drain (remove while we're doing this) so we don't have to immediately clear it in IndexedRingBuffer.releaseToPool? - subscriptions.forEach(UNSUBSCRIBE); - } - - private final static Func1 UNSUBSCRIBE = new Func1() { - - @Override - public Boolean call(Subscription s) { - s.unsubscribe(); - return Boolean.TRUE; - } - }; - -} diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index 1482d34756..9f7b14eb43 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -18,7 +18,6 @@ import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import rx.Scheduler; import rx.Subscription; @@ -47,9 +46,7 @@ public Worker createWorker() { private static class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { - private static final AtomicIntegerFieldUpdater COUNTER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(InnerCurrentThreadScheduler.class, "counter"); - @SuppressWarnings("unused") - volatile int counter; + final AtomicInteger counter = new AtomicInteger(); private final PriorityBlockingQueue queue = new PriorityBlockingQueue(); private final BooleanSubscription innerSubscription = new BooleanSubscription(); private final AtomicInteger wip = new AtomicInteger(); @@ -70,7 +67,7 @@ private Subscription enqueue(Action0 action, long execTime) { if (innerSubscription.isUnsubscribed()) { return Subscriptions.unsubscribed(); } - final TimedAction timedAction = new TimedAction(action, execTime, COUNTER_UPDATER.incrementAndGet(this)); + final TimedAction timedAction = new TimedAction(action, execTime, counter.incrementAndGet()); queue.add(timedAction); if (wip.getAndIncrement() == 0) { diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index e3e508164f..b124b8966c 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -67,7 +67,7 @@ public static AsyncSubject create() { state.onTerminated = new Action1>() { @Override public void call(SubjectObserver o) { - Object v = state.get(); + Object v = state.getLatest(); NotificationLite nl = state.nl; o.accept(v, nl); if (v == null || (!nl.isCompleted(v) && !nl.isError(v))) { @@ -145,7 +145,7 @@ public boolean hasObservers() { @Override public boolean hasValue() { Object v = lastValue; - Object o = state.get(); + Object o = state.getLatest(); return !nl.isError(o) && nl.isNext(v); } /** @@ -155,7 +155,7 @@ public boolean hasValue() { @Experimental @Override public boolean hasThrowable() { - Object o = state.get(); + Object o = state.getLatest(); return nl.isError(o); } /** @@ -165,7 +165,7 @@ public boolean hasThrowable() { @Experimental @Override public boolean hasCompleted() { - Object o = state.get(); + Object o = state.getLatest(); return o != null && !nl.isError(o); } /** @@ -181,7 +181,7 @@ public boolean hasCompleted() { @Override public T getValue() { Object v = lastValue; - Object o = state.get(); + Object o = state.getLatest(); if (!nl.isError(o) && nl.isNext(v)) { return nl.getValue(v); } @@ -195,7 +195,7 @@ public T getValue() { @Experimental @Override public Throwable getThrowable() { - Object o = state.get(); + Object o = state.getLatest(); if (nl.isError(o)) { return nl.getError(o); } @@ -207,7 +207,7 @@ public Throwable getThrowable() { @SuppressWarnings("unchecked") public T[] getValues(T[] a) { Object v = lastValue; - Object o = state.get(); + Object o = state.getLatest(); if (!nl.isError(o) && nl.isNext(v)) { T val = nl.getValue(v); if (a.length == 0) { diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index 218eef5eba..d912e81411 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -97,13 +97,13 @@ public static BehaviorSubject create(T defaultValue) { private static BehaviorSubject create(T defaultValue, boolean hasDefault) { final SubjectSubscriptionManager state = new SubjectSubscriptionManager(); if (hasDefault) { - state.set(NotificationLite.instance().next(defaultValue)); + state.setLatest(NotificationLite.instance().next(defaultValue)); } state.onAdded = new Action1>() { @Override public void call(SubjectObserver o) { - o.emitFirst(state.get(), state.nl); + o.emitFirst(state.getLatest(), state.nl); } }; @@ -121,7 +121,7 @@ protected BehaviorSubject(OnSubscribe onSubscribe, SubjectSubscriptionManager @Override public void onCompleted() { - Object last = state.get(); + Object last = state.getLatest(); if (last == null || state.active) { Object n = nl.completed(); for (SubjectObserver bo : state.terminate(n)) { @@ -132,7 +132,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { - Object last = state.get(); + Object last = state.getLatest(); if (last == null || state.active) { Object n = nl.error(e); List errors = null; @@ -153,7 +153,7 @@ public void onError(Throwable e) { @Override public void onNext(T v) { - Object last = state.get(); + Object last = state.getLatest(); if (last == null || state.active) { Object n = nl.next(v); for (SubjectObserver bo : state.next(n)) { @@ -180,7 +180,7 @@ public boolean hasObservers() { @Experimental @Override public boolean hasValue() { - Object o = state.get(); + Object o = state.getLatest(); return nl.isNext(o); } /** @@ -190,7 +190,7 @@ public boolean hasValue() { @Experimental @Override public boolean hasThrowable() { - Object o = state.get(); + Object o = state.getLatest(); return nl.isError(o); } /** @@ -200,7 +200,7 @@ public boolean hasThrowable() { @Experimental @Override public boolean hasCompleted() { - Object o = state.get(); + Object o = state.getLatest(); return nl.isCompleted(o); } /** @@ -215,7 +215,7 @@ public boolean hasCompleted() { @Experimental @Override public T getValue() { - Object o = state.get(); + Object o = state.getLatest(); if (nl.isNext(o)) { return nl.getValue(o); } @@ -229,7 +229,7 @@ public T getValue() { @Experimental @Override public Throwable getThrowable() { - Object o = state.get(); + Object o = state.getLatest(); if (nl.isError(o)) { return nl.getError(o); } @@ -239,7 +239,7 @@ public Throwable getThrowable() { @Experimental @SuppressWarnings("unchecked") public T[] getValues(T[] a) { - Object o = state.get(); + Object o = state.getLatest(); if (nl.isNext(o)) { if (a.length == 0) { a = (T[])Array.newInstance(a.getClass().getComponentType(), 1); diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index 6ec0af1608..f9dd1f0e4f 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -63,7 +63,7 @@ public static PublishSubject create() { @Override public void call(SubjectObserver o) { - o.emitFirst(state.get(), state.nl); + o.emitFirst(state.getLatest(), state.nl); } }; @@ -127,7 +127,7 @@ public boolean hasObservers() { @Experimental @Override public boolean hasThrowable() { - Object o = state.get(); + Object o = state.getLatest(); return nl.isError(o); } /** @@ -137,7 +137,7 @@ public boolean hasThrowable() { @Experimental @Override public boolean hasCompleted() { - Object o = state.get(); + Object o = state.getLatest(); return o != null && !nl.isError(o); } /** @@ -148,7 +148,7 @@ public boolean hasCompleted() { @Experimental @Override public Throwable getThrowable() { - Object o = state.get(); + Object o = state.getLatest(); if (nl.isError(o)) { return nl.getError(o); } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index f2230f4bba..d683db0b12 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -16,15 +16,17 @@ package rx.subjects; import java.lang.reflect.Array; -import java.util.*; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicInteger; -import rx.*; import rx.Observer; +import rx.Scheduler; import rx.annotations.Experimental; import rx.exceptions.Exceptions; -import rx.functions.*; +import rx.functions.Action1; +import rx.functions.Func1; import rx.internal.operators.NotificationLite; import rx.internal.util.UtilityFunctions; import rx.schedulers.Timestamped; @@ -113,15 +115,17 @@ public void call(SubjectObserver o) { } boolean skipFinal = false; try { + //noinspection UnnecessaryLocalVariable - Avoid re-read from outside this scope + final UnboundedReplayState localState = state; for (;;) { int idx = o.index(); - int sidx = state.index; + int sidx = localState.get(); if (idx != sidx) { - Integer j = state.replayObserverFromIndex(idx, o); + Integer j = localState.replayObserverFromIndex(idx, o); o.index(j); } synchronized (o) { - if (sidx == state.index) { + if (sidx == localState.get()) { o.emitting = false; skipFinal = true; break; @@ -410,7 +414,7 @@ public void onCompleted() { * @return Returns the number of subscribers. */ /* Support test. */int subscriberCount() { - return ssm.state.observers.length; + return ssm.get().observers.length; } @Override @@ -439,17 +443,12 @@ private boolean caughtUp(SubjectObserver o) { * The unbounded replay state. * @param the input and output type */ - static final class UnboundedReplayState implements ReplayState { + static final class UnboundedReplayState extends AtomicInteger implements ReplayState { private final NotificationLite nl = NotificationLite.instance(); /** The buffer. */ private final ArrayList list; /** The termination flag. */ private volatile boolean terminated; - /** The size of the buffer. */ - volatile int index; - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater INDEX_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(UnboundedReplayState.class, "index"); public UnboundedReplayState(int initialCapacity) { list = new ArrayList(initialCapacity); } @@ -458,7 +457,7 @@ public UnboundedReplayState(int initialCapacity) { public void next(T n) { if (!terminated) { list.add(nl.next(n)); - INDEX_UPDATER.getAndIncrement(this); // release index + getAndIncrement(); // release index } } @@ -471,7 +470,7 @@ public void complete() { if (!terminated) { terminated = true; list.add(nl.completed()); - INDEX_UPDATER.getAndIncrement(this); // release index + getAndIncrement(); // release index } } @Override @@ -479,7 +478,7 @@ public void error(Throwable e) { if (!terminated) { terminated = true; list.add(nl.error(e)); - INDEX_UPDATER.getAndIncrement(this); // release index + getAndIncrement(); // release index } } @@ -511,7 +510,7 @@ public boolean replayObserver(SubjectObserver observer) { @Override public Integer replayObserverFromIndex(Integer idx, SubjectObserver observer) { int i = idx; - while (i < index) { + while (i < get()) { accept(observer, i); i++; } @@ -526,7 +525,7 @@ public Integer replayObserverFromIndexTest(Integer idx, SubjectObserver 0) { Object o = list.get(idx - 1); if (nl.isCompleted(o) || nl.isError(o)) { @@ -561,7 +560,7 @@ public T[] toArray(T[] a) { } @Override public T latest() { - int idx = index; + int idx = get(); if (idx > 0) { Object o = list.get(idx - 1); if (nl.isCompleted(o) || nl.isError(o)) { @@ -1102,7 +1101,7 @@ public void evictFinal(NodeList list) { @Override public boolean hasThrowable() { NotificationLite nl = ssm.nl; - Object o = ssm.get(); + Object o = ssm.getLatest(); return nl.isError(o); } /** @@ -1113,7 +1112,7 @@ public boolean hasThrowable() { @Override public boolean hasCompleted() { NotificationLite nl = ssm.nl; - Object o = ssm.get(); + Object o = ssm.getLatest(); return o != null && !nl.isError(o); } /** @@ -1125,7 +1124,7 @@ public boolean hasCompleted() { @Override public Throwable getThrowable() { NotificationLite nl = ssm.nl; - Object o = ssm.get(); + Object o = ssm.getLatest(); if (nl.isError(o)) { return nl.getError(o); } diff --git a/src/main/java/rx/subjects/SubjectSubscriptionManager.java b/src/main/java/rx/subjects/SubjectSubscriptionManager.java index 542d050c39..9a0c90ece7 100644 --- a/src/main/java/rx/subjects/SubjectSubscriptionManager.java +++ b/src/main/java/rx/subjects/SubjectSubscriptionManager.java @@ -17,7 +17,7 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; import rx.Observable.OnSubscribe; import rx.Observer; @@ -33,11 +33,7 @@ * @param the source and return value type */ @SuppressWarnings({"unchecked", "rawtypes"}) -/* package */final class SubjectSubscriptionManager implements OnSubscribe { - /** Contains the unsubscription flag and the array of active subscribers. */ - volatile State state = State.EMPTY; - static final AtomicReferenceFieldUpdater STATE_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(SubjectSubscriptionManager.class, State.class, "state"); +/* package */final class SubjectSubscriptionManager extends AtomicReference> implements OnSubscribe { /** Stores the latest value or the terminal value for some Subjects. */ volatile Object latest; /** Indicates that the subject is active (cheaper than checking the state).*/ @@ -50,6 +46,11 @@ Action1> onTerminated = Actions.empty(); /** The notification lite. */ public final NotificationLite nl = NotificationLite.instance(); + + public SubjectSubscriptionManager() { + super(State.EMPTY); + } + @Override public void call(final Subscriber child) { SubjectObserver bo = new SubjectObserver(child); @@ -71,16 +72,16 @@ public void call() { })); } /** Set the latest NotificationLite value. */ - void set(Object value) { + void setLatest(Object value) { latest = value; } /** @return Retrieve the latest NotificationLite value */ - Object get() { + Object getLatest() { return latest; } /** @return the array of active subscribers, don't write into the array! */ SubjectObserver[] observers() { - return state.observers; + return get().observers; } /** * Try to atomically add a SubjectObserver to the active state. @@ -89,13 +90,13 @@ SubjectObserver[] observers() { */ boolean add(SubjectObserver o) { do { - State oldState = state; + State oldState = get(); if (oldState.terminated) { onTerminated.call(o); return false; } State newState = oldState.add(o); - if (STATE_UPDATER.compareAndSet(this, oldState, newState)) { + if (compareAndSet(oldState, newState)) { onAdded.call(o); return true; } @@ -107,12 +108,12 @@ boolean add(SubjectObserver o) { */ void remove(SubjectObserver o) { do { - State oldState = state; + State oldState = get(); if (oldState.terminated) { return; } State newState = oldState.remove(o); - if (newState == oldState || STATE_UPDATER.compareAndSet(this, oldState, newState)) { + if (newState == oldState || compareAndSet(oldState, newState)) { return; } } while (true); @@ -123,8 +124,8 @@ void remove(SubjectObserver o) { * @return the array of SubjectObservers, don't write into the array! */ SubjectObserver[] next(Object n) { - set(n); - return state.observers; + setLatest(n); + return get().observers; } /** * Atomically set the terminal NotificationLite value (which could be any of the 3), @@ -133,14 +134,14 @@ SubjectObserver[] next(Object n) { * @return the last active SubjectObservers */ SubjectObserver[] terminate(Object n) { - set(n); + setLatest(n); active = false; - State oldState = state; + State oldState = get(); if (oldState.terminated) { return State.NO_OBSERVERS; } - return STATE_UPDATER.getAndSet(this, State.TERMINATED).observers; + return getAndSet(State.TERMINATED).observers; } /** State-machine representing the termination state and active SubjectObservers. */ diff --git a/src/main/java/rx/subjects/TestSubject.java b/src/main/java/rx/subjects/TestSubject.java index 2de860c602..2cc32b007c 100644 --- a/src/main/java/rx/subjects/TestSubject.java +++ b/src/main/java/rx/subjects/TestSubject.java @@ -49,7 +49,7 @@ public static TestSubject create(TestScheduler scheduler) { @Override public void call(SubjectObserver o) { - o.emitFirst(state.get(), state.nl); + o.emitFirst(state.getLatest(), state.nl); } }; diff --git a/src/main/java/rx/subscriptions/BooleanSubscription.java b/src/main/java/rx/subscriptions/BooleanSubscription.java index ef0b082f79..9ba4100a66 100644 --- a/src/main/java/rx/subscriptions/BooleanSubscription.java +++ b/src/main/java/rx/subscriptions/BooleanSubscription.java @@ -15,7 +15,7 @@ */ package rx.subscriptions; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; import rx.Observable; import rx.Subscription; @@ -27,17 +27,14 @@ */ public final class BooleanSubscription implements Subscription { - private final Action0 action; - volatile int unsubscribed; - static final AtomicIntegerFieldUpdater UNSUBSCRIBED_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(BooleanSubscription.class, "unsubscribed"); + final AtomicReference actionRef; public BooleanSubscription() { - action = null; + actionRef = new AtomicReference(); } private BooleanSubscription(Action0 action) { - this.action = action; + actionRef = new AtomicReference(action); } /** @@ -62,16 +59,25 @@ public static BooleanSubscription create(Action0 onUnsubscribe) { @Override public boolean isUnsubscribed() { - return unsubscribed != 0; + return actionRef.get() == EMPTY_ACTION; } @Override public final void unsubscribe() { - if (UNSUBSCRIBED_UPDATER.compareAndSet(this, 0, 1)) { - if (action != null) { + Action0 action = actionRef.get(); + if (action != EMPTY_ACTION) { + action = actionRef.getAndSet(EMPTY_ACTION); + if (action != null && action != EMPTY_ACTION) { action.call(); } } } + static final Action0 EMPTY_ACTION = new Action0() { + @Override + public void call() { + + } + }; + } diff --git a/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java b/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java index 8591b062d7..ec0ea7c6df 100644 --- a/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java +++ b/src/main/java/rx/subscriptions/MultipleAssignmentSubscription.java @@ -15,7 +15,7 @@ */ package rx.subscriptions; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; import rx.Observable; import rx.Subscription; @@ -26,9 +26,7 @@ */ public final class MultipleAssignmentSubscription implements Subscription { - volatile State state = new State(false, Subscriptions.empty()); - static final AtomicReferenceFieldUpdater STATE_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(MultipleAssignmentSubscription.class, State.class, "state"); + final AtomicReference state = new AtomicReference(new State(false, Subscriptions.empty())); private static final class State { final boolean isUnsubscribed; @@ -50,21 +48,22 @@ State set(Subscription s) { } @Override public boolean isUnsubscribed() { - return state.isUnsubscribed; + return state.get().isUnsubscribed; } @Override public void unsubscribe() { State oldState; State newState; + final AtomicReference localState = this.state; do { - oldState = state; + oldState = localState.get(); if (oldState.isUnsubscribed) { return; } else { newState = oldState.unsubscribe(); } - } while (!STATE_UPDATER.compareAndSet(this, oldState, newState)); + } while (!localState.compareAndSet(oldState, newState)); oldState.subscription.unsubscribe(); } @@ -81,15 +80,16 @@ public void set(Subscription s) { } State oldState; State newState; + final AtomicReference localState = this.state; do { - oldState = state; + oldState = localState.get(); if (oldState.isUnsubscribed) { s.unsubscribe(); return; } else { newState = oldState.set(s); } - } while (!STATE_UPDATER.compareAndSet(this, oldState, newState)); + } while (!localState.compareAndSet(oldState, newState)); } /** @@ -98,7 +98,7 @@ public void set(Subscription s) { * @return the {@link Subscription} that underlies the {@code MultipleAssignmentSubscription} */ public Subscription get() { - return state.subscription; + return state.get().subscription; } } diff --git a/src/main/java/rx/subscriptions/RefCountSubscription.java b/src/main/java/rx/subscriptions/RefCountSubscription.java index af225fa1a7..a45c6d3b66 100644 --- a/src/main/java/rx/subscriptions/RefCountSubscription.java +++ b/src/main/java/rx/subscriptions/RefCountSubscription.java @@ -15,8 +15,8 @@ */ package rx.subscriptions; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import rx.Subscription; @@ -27,9 +27,7 @@ public final class RefCountSubscription implements Subscription { private final Subscription actual; static final State EMPTY_STATE = new State(false, 0); - volatile State state = EMPTY_STATE; - static final AtomicReferenceFieldUpdater STATE_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(RefCountSubscription.class, State.class, "state"); + final AtomicReference state = new AtomicReference(EMPTY_STATE); private static final class State { final boolean isUnsubscribed; @@ -77,34 +75,36 @@ public RefCountSubscription(Subscription s) { public Subscription get() { State oldState; State newState; + final AtomicReference localState = this.state; do { - oldState = state; + oldState = localState.get(); if (oldState.isUnsubscribed) { return Subscriptions.unsubscribed(); } else { newState = oldState.addChild(); } - } while (!STATE_UPDATER.compareAndSet(this, oldState, newState)); + } while (!localState.compareAndSet(oldState, newState)); return new InnerSubscription(this); } @Override public boolean isUnsubscribed() { - return state.isUnsubscribed; + return state.get().isUnsubscribed; } @Override public void unsubscribe() { State oldState; State newState; + final AtomicReference localState = this.state; do { - oldState = state; + oldState = localState.get(); if (oldState.isUnsubscribed) { return; } newState = oldState.unsubscribe(); - } while (!STATE_UPDATER.compareAndSet(this, oldState, newState)); + } while (!localState.compareAndSet(oldState, newState)); unsubscribeActualIfApplicable(newState); } @@ -116,32 +116,30 @@ private void unsubscribeActualIfApplicable(State state) { void unsubscribeAChild() { State oldState; State newState; + final AtomicReference localState = this.state; do { - oldState = state; + oldState = localState.get(); newState = oldState.removeChild(); - } while (!STATE_UPDATER.compareAndSet(this, oldState, newState)); + } while (!localState.compareAndSet(oldState, newState)); unsubscribeActualIfApplicable(newState); } /** The individual sub-subscriptions. */ - private static final class InnerSubscription implements Subscription { + private static final class InnerSubscription extends AtomicInteger implements Subscription { final RefCountSubscription parent; - volatile int innerDone; - static final AtomicIntegerFieldUpdater INNER_DONE_UPDATER - = AtomicIntegerFieldUpdater.newUpdater(InnerSubscription.class, "innerDone"); public InnerSubscription(RefCountSubscription parent) { this.parent = parent; } @Override public void unsubscribe() { - if (INNER_DONE_UPDATER.compareAndSet(this, 0, 1)) { + if (compareAndSet(0, 1)) { parent.unsubscribeAChild(); } } @Override public boolean isUnsubscribed() { - return innerDone != 0; + return get() != 0; } }; } diff --git a/src/main/java/rx/subscriptions/SerialSubscription.java b/src/main/java/rx/subscriptions/SerialSubscription.java index 6cc5019092..f8aff9b67e 100644 --- a/src/main/java/rx/subscriptions/SerialSubscription.java +++ b/src/main/java/rx/subscriptions/SerialSubscription.java @@ -15,7 +15,7 @@ */ package rx.subscriptions; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.atomic.AtomicReference; import rx.Subscription; @@ -24,9 +24,7 @@ * the previous underlying subscription to be unsubscribed. */ public final class SerialSubscription implements Subscription { - volatile State state = new State(false, Subscriptions.empty()); - static final AtomicReferenceFieldUpdater STATE_UPDATER - = AtomicReferenceFieldUpdater.newUpdater(SerialSubscription.class, State.class, "state"); + final AtomicReference state = new AtomicReference(new State(false, Subscriptions.empty())); private static final class State { final boolean isUnsubscribed; @@ -49,21 +47,22 @@ State set(Subscription s) { @Override public boolean isUnsubscribed() { - return state.isUnsubscribed; + return state.get().isUnsubscribed; } @Override public void unsubscribe() { State oldState; State newState; + final AtomicReference localState = this.state; do { - oldState = state; + oldState = localState.get(); if (oldState.isUnsubscribed) { return; } else { newState = oldState.unsubscribe(); } - } while (!STATE_UPDATER.compareAndSet(this, oldState, newState)); + } while (!localState.compareAndSet(oldState, newState)); oldState.subscription.unsubscribe(); } @@ -81,15 +80,16 @@ public void set(Subscription s) { } State oldState; State newState; + final AtomicReference localState = this.state; do { - oldState = state; + oldState = localState.get(); if (oldState.isUnsubscribed) { s.unsubscribe(); return; } else { newState = oldState.set(s); } - } while (!STATE_UPDATER.compareAndSet(this, oldState, newState)); + } while (!localState.compareAndSet(oldState, newState)); oldState.subscription.unsubscribe(); } @@ -99,7 +99,7 @@ public void set(Subscription s) { * @return the current {@link Subscription} that is being represented by this {@code SerialSubscription} */ public Subscription get() { - return state.subscription; + return state.get().subscription; } } From f2a96942167df242ea82396cfd5e1b902f113d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 11 Nov 2015 12:55:15 +0100 Subject: [PATCH 443/857] 1.x: BlockingUtils test: clear interrupted flag before/after For some strange reason, the interrupted flag is sometimes still set when the next JUnit test method runs and `await` will throw immediately. --- .../java/rx/internal/util/BlockingUtilsTest.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/test/java/rx/internal/util/BlockingUtilsTest.java b/src/test/java/rx/internal/util/BlockingUtilsTest.java index ff430c7aee..02047b9d2f 100644 --- a/src/test/java/rx/internal/util/BlockingUtilsTest.java +++ b/src/test/java/rx/internal/util/BlockingUtilsTest.java @@ -16,23 +16,29 @@ package rx.internal.util; -import static org.mockito.Mockito.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; -import org.junit.Test; +import org.junit.*; -import rx.Observable; -import rx.Subscriber; -import rx.Subscription; +import rx.*; import rx.schedulers.Schedulers; /** * Test suite for {@link BlockingUtils}. */ public class BlockingUtilsTest { + + @Before + @After + public void before() { + // make sure the interrupted flag is cleared + Thread.interrupted(); + } + @Test public void awaitCompleteShouldReturnIfCountIsZero() { Subscription subscription = mock(Subscription.class); From ee91a9d1172afa3c0a68cdba50abd172a90c809d Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Fri, 9 Oct 2015 01:55:10 +0300 Subject: [PATCH 444/857] Add Single.defer() --- src/main/java/rx/Single.java | 44 +++++++++++ src/test/java/rx/SingleTest.java | 129 +++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index b126fd39a3..f862d42e0c 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1953,4 +1953,48 @@ public final Single delay(long delay, TimeUnit unit, Scheduler scheduler) { public final Single delay(long delay, TimeUnit unit) { return delay(delay, unit, Schedulers.computation()); } + + /** + * Returns a {@link Single} that calls a {@link Single} factory to create a {@link Single} for each new Observer + * that subscribes. That is, for each subscriber, the actual {@link Single} that subscriber observes is + * determined by the factory function. + *

+ * + *

+ * The defer Observer allows you to defer or delay emitting value from a {@link Single} until such time as an + * Observer subscribes to the {@link Single}. This allows an {@link Observer} to easily obtain updates or a + * refreshed version of the sequence. + *

+ *
Scheduler:
+ *
{@code defer} does not operate by default on a particular {@link Scheduler}.
+ *
+ * + * @param singleFactory + * the {@link Single} factory function to invoke for each {@link Observer} that subscribes to the + * resulting {@link Single}. + * @param + * the type of the items emitted by the {@link Single}. + * @return a {@link Single} whose {@link Observer}s' subscriptions trigger an invocation of the given + * {@link Single} factory function. + * @see ReactiveX operators documentation: Defer + */ + @Experimental + public static Single defer(final Callable> singleFactory) { + return create(new OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + Single single; + + try { + single = singleFactory.call(); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + singleSubscriber.onError(t); + return; + } + + single.subscribe(singleSubscriber); + } + }); + } } diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 5bc24a6368..30fe99e92f 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -20,6 +20,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -30,10 +31,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import rx.Single.OnSubscribe; import rx.exceptions.CompositeException; import rx.functions.Action0; @@ -699,4 +703,129 @@ public void call(SingleSubscriber singleSubscriber) { subscriber.assertNoValues(); subscriber.assertError(expected); } + + @Test + public void deferShouldNotCallFactoryFuncUntilSubscriberSubscribes() throws Exception { + Callable> singleFactory = mock(Callable.class); + Single.defer(singleFactory); + verifyZeroInteractions(singleFactory); + } + + @Test + public void deferShouldSubscribeSubscriberToSingleFromFactoryFuncAndEmitValue() throws Exception { + Callable> singleFactory = mock(Callable.class); + Object value = new Object(); + Single single = Single.just(value); + + when(singleFactory.call()).thenReturn(single); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .defer(singleFactory) + .subscribe(testSubscriber); + + testSubscriber.assertValue(value); + testSubscriber.assertNoErrors(); + + verify(singleFactory).call(); + } + + @Test + public void deferShouldSubscribeSubscriberToSingleFromFactoryFuncAndEmitError() throws Exception { + Callable> singleFactory = mock(Callable.class); + Throwable error = new IllegalStateException(); + Single single = Single.error(error); + + when(singleFactory.call()).thenReturn(single); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .defer(singleFactory) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(error); + + verify(singleFactory).call(); + } + + @Test + public void deferShouldPassErrorFromSingleFactoryToTheSubscriber() throws Exception { + Callable> singleFactory = mock(Callable.class); + Throwable errorFromSingleFactory = new IllegalStateException(); + when(singleFactory.call()).thenThrow(errorFromSingleFactory); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .defer(singleFactory) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(errorFromSingleFactory); + + verify(singleFactory).call(); + } + + @Test + public void deferShouldCallSingleFactoryForEachSubscriber() throws Exception { + Callable> singleFactory = mock(Callable.class); + + String[] values = {"1", "2", "3"}; + final Single[] singles = new Single[]{Single.just(values[0]), Single.just(values[1]), Single.just(values[2])}; + + final AtomicInteger singleFactoryCallsCounter = new AtomicInteger(); + + when(singleFactory.call()).thenAnswer(new Answer>() { + @Override + public Single answer(InvocationOnMock invocation) throws Throwable { + return singles[singleFactoryCallsCounter.getAndIncrement()]; + } + }); + + Single deferredSingle = Single.defer(singleFactory); + + for (int i = 0; i < singles.length; i ++) { + TestSubscriber testSubscriber = new TestSubscriber(); + + deferredSingle.subscribe(testSubscriber); + + testSubscriber.assertValue(values[i]); + testSubscriber.assertNoErrors(); + } + + verify(singleFactory, times(3)).call(); + } + + @Test + public void deferShouldPassNullPointerExceptionToTheSubscriberIfSingleFactoryIsNull() { + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .defer(null) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(NullPointerException.class); + } + + + @Test + public void deferShouldPassNullPointerExceptionToTheSubscriberIfSingleFactoryReturnsNull() throws Exception { + Callable> singleFactory = mock(Callable.class); + when(singleFactory.call()).thenReturn(null); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .defer(singleFactory) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(NullPointerException.class); + + verify(singleFactory).call(); + } } From c7e2ecf90c4850ab8f7c6bd33eab8bb1e6db01e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 11 Nov 2015 19:47:25 +0100 Subject: [PATCH 445/857] 1.x: add shorter RxJavaPlugin class lookup approach. --- src/main/java/rx/plugins/RxJavaPlugins.java | 66 ++++++++++++++++--- .../java/rx/plugins/RxJavaPluginsTest.java | 46 +++++++++---- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/main/java/rx/plugins/RxJavaPlugins.java b/src/main/java/rx/plugins/RxJavaPlugins.java index 2e48305989..09e542779d 100644 --- a/src/main/java/rx/plugins/RxJavaPlugins.java +++ b/src/main/java/rx/plugins/RxJavaPlugins.java @@ -15,6 +15,7 @@ */ package rx.plugins; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; /** @@ -26,7 +27,22 @@ * property names) *
  • default implementation
  • * - * + *

    In addition to the {@code rxjava.plugin.[simple classname].implementation} system properties, + * you can define two system property:
    + *

    
    + * rxjava.plugin.[index].class}
    + * rxjava.plugin.[index].impl}
    + * 
    + * + * 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 + * {@code RxJavaErrorHandler} via two system property: + *
    
    + * rxjava.plugin.1.class=RxJavaErrorHandler
    + * rxjava.plugin.1.impl=some.package.MyRxJavaErrorHandler
    + * 
    + * * @see RxJava Wiki: Plugins */ public class RxJavaPlugins { @@ -64,13 +80,12 @@ public static RxJavaPlugins getInstance() { *

    * Override the default by calling {@link #registerErrorHandler(RxJavaErrorHandler)} or by setting the * property {@code rxjava.plugin.RxJavaErrorHandler.implementation} with the full classname to load. - * * @return {@link RxJavaErrorHandler} implementation to use */ public RxJavaErrorHandler getErrorHandler() { if (errorHandler.get() == null) { // check for an implementation from System.getProperty first - Object impl = getPluginImplementationViaProperty(RxJavaErrorHandler.class); + Object impl = getPluginImplementationViaProperty(RxJavaErrorHandler.class, System.getProperties()); if (impl == null) { // nothing set via properties so initialize with default errorHandler.compareAndSet(null, DEFAULT_ERROR_HANDLER); @@ -112,7 +127,7 @@ public void registerErrorHandler(RxJavaErrorHandler impl) { public RxJavaObservableExecutionHook getObservableExecutionHook() { if (observableExecutionHook.get() == null) { // check for an implementation from System.getProperty first - Object impl = getPluginImplementationViaProperty(RxJavaObservableExecutionHook.class); + Object impl = getPluginImplementationViaProperty(RxJavaObservableExecutionHook.class, System.getProperties()); if (impl == null) { // nothing set via properties so initialize with default observableExecutionHook.compareAndSet(null, RxJavaObservableExecutionHookDefault.getInstance()); @@ -141,15 +156,46 @@ public void registerObservableExecutionHook(RxJavaObservableExecutionHook impl) } } - private static Object getPluginImplementationViaProperty(Class pluginClass) { - String classSimpleName = pluginClass.getSimpleName(); + /* test */ static Object getPluginImplementationViaProperty(Class pluginClass, Properties props) { + final String classSimpleName = pluginClass.getSimpleName(); /* * Check system properties for plugin class. *

    * This will only happen during system startup thus it's okay to use the synchronized * System.getProperties as it will never get called in normal operations. */ - String implementingClass = System.getProperty("rxjava.plugin." + classSimpleName + ".implementation"); + + final String pluginPrefix = "rxjava.plugin."; + + String defaultKey = pluginPrefix + classSimpleName + ".implementation"; + String implementingClass = props.getProperty(defaultKey); + + if (implementingClass == null) { + final String classSuffix = ".class"; + final String implSuffix = ".impl"; + + for (Map.Entry e : props.entrySet()) { + String key = e.getKey().toString(); + if (key.startsWith(pluginPrefix) && key.endsWith(classSuffix)) { + String value = e.getValue().toString(); + + if (classSimpleName.equals(value)) { + String index = key.substring(0, key.length() - classSuffix.length()).substring(pluginPrefix.length()); + + String implKey = pluginPrefix + index + implSuffix; + + implementingClass = props.getProperty(implKey); + + if (implementingClass == null) { + throw new RuntimeException("Implementing class declaration for " + classSimpleName + " missing: " + implKey); + } + + break; + } + } + } + } + if (implementingClass != null) { try { Class cls = Class.forName(implementingClass); @@ -165,9 +211,9 @@ private static Object getPluginImplementationViaProperty(Class pluginClass) { } catch (IllegalAccessException e) { throw new RuntimeException(classSimpleName + " implementation not able to be accessed: " + implementingClass, e); } - } else { - return null; } + + return null; } /** @@ -183,7 +229,7 @@ private static Object getPluginImplementationViaProperty(Class pluginClass) { public RxJavaSchedulersHook getSchedulersHook() { if (schedulersHook.get() == null) { // check for an implementation from System.getProperty first - Object impl = getPluginImplementationViaProperty(RxJavaSchedulersHook.class); + Object impl = getPluginImplementationViaProperty(RxJavaSchedulersHook.class, System.getProperties()); if (impl == null) { // nothing set via properties so initialize with default schedulersHook.compareAndSet(null, RxJavaSchedulersHook.getDefaultInstance()); diff --git a/src/test/java/rx/plugins/RxJavaPluginsTest.java b/src/test/java/rx/plugins/RxJavaPluginsTest.java index 3d18923915..e4cd9f69ae 100644 --- a/src/test/java/rx/plugins/RxJavaPluginsTest.java +++ b/src/test/java/rx/plugins/RxJavaPluginsTest.java @@ -15,21 +15,12 @@ */ package rx.plugins; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; +import static org.junit.Assert.*; + +import java.util.*; import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import rx.Observable; import rx.Subscriber; @@ -251,4 +242,33 @@ private static String getFullClassNameForTestClass(Class cls) { return RxJavaPlugins.class.getPackage() .getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName(); } + + @Test + public void testShortPluginDiscovery() { + Properties props = new Properties(); + + props.setProperty("rxjava.plugin.1.class", "Map"); + props.setProperty("rxjava.plugin.1.impl", "java.util.HashMap"); + + props.setProperty("rxjava.plugin.xyz.class", "List"); + props.setProperty("rxjava.plugin.xyz.impl", "java.util.ArrayList"); + + + Object o = RxJavaPlugins.getPluginImplementationViaProperty(Map.class, props); + + assertTrue("" + o, o instanceof HashMap); + + o = RxJavaPlugins.getPluginImplementationViaProperty(List.class, props); + + assertTrue("" + o, o instanceof ArrayList); + } + + @Test(expected = RuntimeException.class) + public void testShortPluginDiscoveryMissing() { + Properties props = new Properties(); + + props.setProperty("rxjava.plugin.1.class", "Map"); + + RxJavaPlugins.getPluginImplementationViaProperty(Map.class, props); + } } From b8710ab5ce22032d62af79e040bb1d56fb585d28 Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Wed, 11 Nov 2015 12:36:27 -0800 Subject: [PATCH 446/857] Update CHANGES.md for v1.0.16 --- CHANGES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 444b8f33e2..5fa01c46ec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,16 @@ # RxJava Releases # +### Version 1.0.16 – November 11 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.16%7C)) ### +* [Pull 3169] (https://github.com/ReactiveX/RxJava/pull/3169) Merge can now operate in horizontally unbounded mode +* [Pull 3286] (https://github.com/ReactiveX/RxJava/pull/3286) Implements BlockingSingle +* [Pull 3433] (https://github.com/ReactiveX/RxJava/pull/3433) Add Single.defer() +* [Pull 3468] (https://github.com/ReactiveX/RxJava/pull/3468) Fix other places that may swallow OnErrorFailedException +* [Pull 3485] (https://github.com/ReactiveX/RxJava/pull/3485) fix scan() not accepting a null initial value +* [Pull 3488] (https://github.com/ReactiveX/RxJava/pull/3488) Replace all instances of Atomic*FieldUpdater with direct Atomic* instances +* [Pull 3493] (https://github.com/ReactiveX/RxJava/pull/3493) fix for zip(Obs>) backpressure problem +* [Pull 3510] (https://github.com/ReactiveX/RxJava/pull/3510) eager concatMap to choose safe or unsafe queue based on platform +* [Pull 3512] (https://github.com/ReactiveX/RxJava/pull/3512) fix SafeSubscriber documentation regarding unsubscribe + ### Version 1.0.15 – October 9 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.15%7C)) ### * [Pull 3438] (https://github.com/ReactiveX/RxJava/pull/3438) Better null tolerance in rx.exceptions.*Exception classes From cdffb6e6fe656e9bd6ba6e47335a8de3b0046921 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 14 Nov 2015 15:40:26 +1100 Subject: [PATCH 447/857] amend javadocs for Observable.subscribe() as per #3523 --- src/main/java/rx/Observable.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index cf1686ad83..19c5958005 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -7929,7 +7929,8 @@ public final Observable startWith(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T } /** - * Subscribes to an Observable but ignore its emissions and notifications. + * Subscribes to an Observable and ignores {@code onNext} and {@code onCompleted} emissions. If an {@code onError} emission arrives then + * {@link OnErrorNotImplementedException} is thrown. *

    *
    Scheduler:
    *
    {@code subscribe} does not operate by default on a particular {@link Scheduler}.
    From 2fc3e986323b4f5702c8e881d5078bb653affeb2 Mon Sep 17 00:00:00 2001 From: Shixiong Zhu Date: Sun, 15 Nov 2015 21:46:42 -0800 Subject: [PATCH 448/857] Avoid to call next when Iterator is drained --- .../operators/OperatorZipIterable.java | 13 +++++++++--- .../operators/OperatorZipIterableTest.java | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorZipIterable.java b/src/main/java/rx/internal/operators/OperatorZipIterable.java index f913854d1d..056522998c 100644 --- a/src/main/java/rx/internal/operators/OperatorZipIterable.java +++ b/src/main/java/rx/internal/operators/OperatorZipIterable.java @@ -46,23 +46,30 @@ public Subscriber call(final Subscriber subscriber) { return Subscribers.empty(); } return new Subscriber(subscriber) { - boolean once; + boolean done; @Override public void onCompleted() { - if (once) { + if (done) { return; } - once = true; + done = true; subscriber.onCompleted(); } @Override public void onError(Throwable e) { + if (done) { + return; + } + done = true; subscriber.onError(e); } @Override public void onNext(T1 t) { + if (done) { + return; + } try { subscriber.onNext(zipFunction.call(t, iterator.next())); if (!iterator.hasNext()) { diff --git a/src/test/java/rx/internal/operators/OperatorZipIterableTest.java b/src/test/java/rx/internal/operators/OperatorZipIterableTest.java index 15ae12570d..2aa2ed9a9d 100644 --- a/src/test/java/rx/internal/operators/OperatorZipIterableTest.java +++ b/src/test/java/rx/internal/operators/OperatorZipIterableTest.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Iterator; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; @@ -37,6 +38,8 @@ import rx.functions.Func1; import rx.functions.Func2; import rx.functions.Func3; +import rx.observers.TestSubscriber; +import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; public class OperatorZipIterableTest { @@ -378,4 +381,22 @@ public String call(Integer t1) { assertEquals(2, squareStr.counter.get()); } + + @Test + public void testZipIterableWithDelay() { + TestScheduler scheduler = new TestScheduler(); + Observable o = Observable.just(1, 2).zipWith(Arrays.asList(1), new Func2() { + @Override + public Integer call(Integer v1, Integer v2) { + return v1; + } + }).delay(500, TimeUnit.MILLISECONDS, scheduler); + + TestSubscriber subscriber = new TestSubscriber(); + o.subscribe(subscriber); + scheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS); + subscriber.assertValue(1); + subscriber.assertNoErrors(); + subscriber.assertCompleted(); + } } From ce889d6b18ee1ed6de473098767e93c88e10b2b8 Mon Sep 17 00:00:00 2001 From: Shixiong Zhu Date: Sat, 21 Nov 2015 19:27:38 -0800 Subject: [PATCH 449/857] Don't swallow fatal errors in OperatorZipIterable --- src/main/java/rx/internal/operators/OperatorZipIterable.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/rx/internal/operators/OperatorZipIterable.java b/src/main/java/rx/internal/operators/OperatorZipIterable.java index 056522998c..a90e62f470 100644 --- a/src/main/java/rx/internal/operators/OperatorZipIterable.java +++ b/src/main/java/rx/internal/operators/OperatorZipIterable.java @@ -59,6 +59,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { if (done) { + Exceptions.throwIfFatal(e); return; } done = true; From f238c8957c995da567c7fb421b2263aea756d18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Sun, 22 Nov 2015 10:22:40 +0100 Subject: [PATCH 450/857] 1.x: Fix SyncOnSubscribeTest.testConcurrentRequests non-determinism The test checks if onUnSubscribe is called but that happens after onCompleted is sent and as such, may run concurrently with the main thread where the mock is verified. The change switches to CountDownLatch to properly await the call to onUnsubscribe. --- .../java/rx/observables/SyncOnSubscribeTest.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/test/java/rx/observables/SyncOnSubscribeTest.java b/src/test/java/rx/observables/SyncOnSubscribeTest.java index 365e457c81..82cfc0b033 100644 --- a/src/test/java/rx/observables/SyncOnSubscribeTest.java +++ b/src/test/java/rx/observables/SyncOnSubscribeTest.java @@ -476,8 +476,14 @@ public void testConcurrentRequests() throws InterruptedException { final CountDownLatch l1 = new CountDownLatch(1); final CountDownLatch l2 = new CountDownLatch(1); - @SuppressWarnings("unchecked") - Action1 onUnSubscribe = mock(Action1.class); + final CountDownLatch l3 = new CountDownLatch(1); + + final Action1 onUnSubscribe = new Action1() { + @Override + public void call(Object t) { + l3.countDown(); + } + }; OnSubscribe os = SyncOnSubscribe.createStateful( new Func0() { @@ -532,7 +538,10 @@ public Integer call(Integer state, Observer observer) { inOrder.verify(o, times(finalCount)).onNext(any()); inOrder.verify(o, times(1)).onCompleted(); inOrder.verifyNoMoreInteractions(); - verify(onUnSubscribe, times(1)).call(any(Integer.class)); + + if (!l3.await(2, TimeUnit.SECONDS)) { + fail("SyncOnSubscribe failed to countDown onUnSubscribe latch"); + } } @Test From e30a333bb461cc5444501dedadcd8a04ca61ce82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 23 Nov 2015 23:15:14 +0100 Subject: [PATCH 451/857] Window operator now supports backpressure in the inner observable --- .../OperatorWindowWithObservable.java | 2 +- .../OperatorWindowWithObservableFactory.java | 2 +- .../operators/OperatorWindowWithSize.java | 6 +- .../OperatorWindowWithStartEndObservable.java | 2 +- .../operators/OperatorWindowWithTime.java | 4 +- .../rx/internal/operators/UnicastSubject.java | 333 ++++++++++++++++++ .../atomic/SpscUnboundedAtomicArrayQueue.java | 319 +++++++++++++++++ .../util/unsafe/QueueProgressIndicators.java | 54 +++ .../util/unsafe/SpscUnboundedArrayQueue.java | 290 +++++++++++++++ .../operators/BufferUntilSubscriberTest.java | 115 +++++- .../operators/OperatorConcatTest.java | 4 +- .../OperatorWindowWithObservableTest.java | 8 +- 12 files changed, 1117 insertions(+), 22 deletions(-) create mode 100644 src/main/java/rx/internal/operators/UnicastSubject.java create mode 100644 src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java create mode 100644 src/main/java/rx/internal/util/unsafe/QueueProgressIndicators.java create mode 100644 src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java b/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java index 3b7e1c1cac..4b2cbb7dd8 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithObservable.java @@ -154,7 +154,7 @@ void replaceSubject() { child.onNext(producer); } void createNewWindow() { - BufferUntilSubscriber bus = BufferUntilSubscriber.create(); + UnicastSubject bus = UnicastSubject.create(); consumer = bus; producer = bus; } diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java b/src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java index a764850c79..5ac748e6f1 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithObservableFactory.java @@ -160,7 +160,7 @@ void replaceSubject() { child.onNext(producer); } void createNewWindow() { - BufferUntilSubscriber bus = BufferUntilSubscriber.create(); + UnicastSubject bus = UnicastSubject.create(); consumer = bus; producer = bus; Observable other; diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java index 62763f1948..06d7c7a583 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithSize.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithSize.java @@ -60,7 +60,7 @@ public Subscriber call(Subscriber> child) { final class ExactSubscriber extends Subscriber { final Subscriber> child; int count; - BufferUntilSubscriber window; + UnicastSubject window; volatile boolean noWindow = true; public ExactSubscriber(Subscriber> child) { /** @@ -107,7 +107,7 @@ void requestMore(long n) { public void onNext(T t) { if (window == null) { noWindow = false; - window = BufferUntilSubscriber.create(); + window = UnicastSubject.create(); child.onNext(window); } window.onNext(t); @@ -242,7 +242,7 @@ public void onCompleted() { } CountedSubject createCountedSubject() { - final BufferUntilSubscriber bus = BufferUntilSubscriber.create(); + final UnicastSubject bus = UnicastSubject.create(); return new CountedSubject(bus, bus); } } diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java b/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java index 82d1474163..07dea16e76 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithStartEndObservable.java @@ -233,7 +233,7 @@ void endWindow(SerializedSubject window) { } } SerializedSubject createSerializedSubject() { - BufferUntilSubscriber bus = BufferUntilSubscriber.create(); + UnicastSubject bus = UnicastSubject.create(); return new SerializedSubject(bus, bus); } } diff --git a/src/main/java/rx/internal/operators/OperatorWindowWithTime.java b/src/main/java/rx/internal/operators/OperatorWindowWithTime.java index cac94c5ba0..d55f0db31c 100644 --- a/src/main/java/rx/internal/operators/OperatorWindowWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorWindowWithTime.java @@ -214,7 +214,7 @@ boolean replaceSubject() { unsubscribe(); return false; } - BufferUntilSubscriber bus = BufferUntilSubscriber.create(); + UnicastSubject bus = UnicastSubject.create(); state = state.create(bus, bus); child.onNext(bus); return true; @@ -492,7 +492,7 @@ void terminateChunk(CountedSerializedSubject chunk) { } } CountedSerializedSubject createCountedSerializedSubject() { - BufferUntilSubscriber bus = BufferUntilSubscriber.create(); + UnicastSubject bus = UnicastSubject.create(); return new CountedSerializedSubject(bus, bus); } } diff --git a/src/main/java/rx/internal/operators/UnicastSubject.java b/src/main/java/rx/internal/operators/UnicastSubject.java new file mode 100644 index 0000000000..44bba2b90e --- /dev/null +++ b/src/main/java/rx/internal/operators/UnicastSubject.java @@ -0,0 +1,333 @@ +/** + * 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.Queue; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.exceptions.*; +import rx.functions.*; +import rx.internal.util.atomic.*; +import rx.internal.util.unsafe.*; +import rx.subjects.Subject; +import rx.subscriptions.Subscriptions; + +/** + * A Subject variant which buffers events until a single Subscriber arrives and replays them to it + * and potentially switches to direct delivery once the Subscriber caught up and requested an unlimited + * 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. + */ +public final class UnicastSubject extends Subject { + + /** + * Constructs an empty UnicastSubject instance with the default capacity hint of 16 elements. + * + * @return the created UnicastSubject instance + */ + public static UnicastSubject create() { + return create(16); + } + /** + * Constructs an empty UnicastSubject instance with a capacity hint. + *

    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 capacityHint the capacity hint for the internal queue + * @return the created BufferUntilSubscriber instance + */ + public static UnicastSubject create(int capacityHint) { + State state = new State(capacityHint); + return new UnicastSubject(state); + } + + final State state; + + private UnicastSubject(State state) { + super(state); + this.state = state; + } + + @Override + public void onNext(T t) { + state.onNext(t); + } + + @Override + public void onError(Throwable e) { + state.onError(e); + } + + @Override + public void onCompleted() { + state.onCompleted(); + } + + @Override + public boolean hasObservers() { + return state.subscriber.get() != null; + } + + /** + * The single-consumption replaying state. + * + * @param the value type + */ + static final class State extends AtomicLong implements Producer, Observer, Action0, OnSubscribe { + /** */ + private static final long serialVersionUID = -9044104859202255786L; + /** The single subscriber. */ + final AtomicReference> subscriber; + /** The queue holding values until the subscriber arrives and catches up. */ + final Queue queue; + /** JCTools queues don't accept nulls. */ + final NotificationLite nl; + /** In case the source emitted an error. */ + Throwable error; + /** Indicates the source has terminated. */ + volatile boolean done; + /** Emitter loop: emitting indicator. Guarded by this. */ + boolean emitting; + /** Emitter loop: missed emission indicator. Guarded by this. */ + boolean missed; + /** Indicates the queue can be bypassed because the child has caught up with the replay. */ + volatile boolean caughtUp; + /** + * Constructor. + * @param capacityHint indicates how large each island in the Spsc queue should be to + * reduce allocation frequency + */ + public State(int capacityHint) { + this.nl = NotificationLite.instance(); + this.subscriber = new AtomicReference>(); + Queue q; + if (capacityHint > 1) { + q = UnsafeAccess.isUnsafeAvailable() + ? new SpscUnboundedArrayQueue(capacityHint) + : new SpscUnboundedAtomicArrayQueue(capacityHint); + } else { + q = UnsafeAccess.isUnsafeAvailable() + ? new SpscLinkedQueue() + : new SpscLinkedAtomicQueue(); + } + this.queue = q; + } + + @Override + public void onNext(T t) { + if (!done) { + if (!caughtUp) { + boolean stillReplay = false; + /* + * We need to offer while holding the lock because + * we have to atomically switch caughtUp to true + * that can only happen if there isn't any concurrent + * offer() happening while the emission is in replayLoop(). + */ + synchronized (this) { + if (!caughtUp) { + queue.offer(nl.next(t)); + stillReplay = true; + } + } + if (stillReplay) { + replay(); + return; + } + } + Subscriber s = subscriber.get(); + try { + s.onNext(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + s.onError(OnErrorThrowable.addValueAsLastCause(ex, t)); + } + } + } + @Override + public void onError(Throwable e) { + if (!done) { + error = e; + done = true; + if (!caughtUp) { + boolean stillReplay = false; + synchronized (this) { + stillReplay = !caughtUp; + } + if (stillReplay) { + replay(); + return; + } + } + subscriber.get().onError(e); + } + } + @Override + public void onCompleted() { + if (!done) { + done = true; + if (!caughtUp) { + boolean stillReplay = false; + synchronized (this) { + stillReplay = !caughtUp; + } + if (stillReplay) { + replay(); + return; + } + } + subscriber.get().onCompleted(); + } + } + + @Override + public void request(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 required"); + } else + if (n > 0L) { + BackpressureUtils.getAndAddRequest(this, n); + replay(); + } else + if (done) { // terminal events can be delivered for zero requests + replay(); + } + } + /** + * Tries to set the given subscriber if not already set, sending an + * IllegalStateException to the subscriber otherwise. + * @param subscriber + */ + @Override + public void call(Subscriber subscriber) { + if (this.subscriber.compareAndSet(null, subscriber)) { + subscriber.add(Subscriptions.create(this)); + subscriber.setProducer(this); + } else { + subscriber.onError(new IllegalStateException("Only a single subscriber is allowed")); + } + } + /** + * Tries to replay the contents of the queue. + */ + void replay() { + synchronized (this) { + if (emitting) { + missed = true; + return; + } + emitting = true; + } + Queue q = queue; + for (;;) { + Subscriber s = subscriber.get(); + boolean unlimited = false; + if (s != null) { + boolean d = done; + boolean empty = q.isEmpty(); + + if (checkTerminated(d, empty, s)) { + return; + } + long r = get(); + unlimited = r == Long.MAX_VALUE; + long e = 0L; + + while (r != 0) { + d = done; + Object v = q.poll(); + empty = v == null; + if (checkTerminated(d, empty, s)) { + return; + } + if (empty) { + break; + } + T value = nl.getValue(v); + try { + s.onNext(value); + } catch (Throwable ex) { + q.clear(); + Exceptions.throwIfFatal(ex); + s.onError(OnErrorThrowable.addValueAsLastCause(ex, value)); + return; + } + r--; + e++; + } + if (!unlimited && e != 0L) { + addAndGet(-e); + } + } + + synchronized (this) { + if (!missed) { + if (unlimited && q.isEmpty()) { + caughtUp = true; + } + emitting = false; + return; + } + missed = false; + } + } + } + /** + * Terminates the state by setting the done flag and tries to clear the queue. + * Should be called only when the child unsubscribes + */ + @Override + public void call() { + done = true; + synchronized (this) { + if (emitting) { + return; + } + emitting = true; + } + queue.clear(); + } + /** + * Checks if one of the terminal conditions have been met: child unsubscribed, + * an error happened or the source terminated and the queue is empty + * @param done + * @param empty + * @param s + * @return + */ + boolean checkTerminated(boolean done, boolean empty, Subscriber s) { + if (s.isUnsubscribed()) { + queue.clear(); + return true; + } + if (done) { + Throwable e = error; + if (e != null) { + queue.clear(); + s.onError(e); + return true; + } else + if (empty) { + s.onCompleted(); + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java new file mode 100644 index 0000000000..af62a9ce60 --- /dev/null +++ b/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java @@ -0,0 +1,319 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic/SpscUnboundedAtomicArrayQueue.java + */ + +package rx.internal.util.atomic; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import rx.internal.util.unsafe.Pow2; + +/** + * A single-producer single-consumer queue with unbounded capacity. + *

    The implementation uses fixed, power-of-2 arrays to store elements and turns into a linked-list like + * structure if the production overshoots the consumption. + *

    Note that the minimum capacity of the 'islands' are 8 due to how the look-ahead optimization works. + *

    The implementation uses field updaters and thus should be platform-safe. + */ +public final class SpscUnboundedAtomicArrayQueue 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(SpscUnboundedAtomicArrayQueue.class, "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(SpscUnboundedAtomicArrayQueue.class, "consumerIndex"); + private static final Object HAS_NEXT = new Object(); + + public SpscUnboundedAtomicArrayQueue(final int bufferSize) { + int p2capacity = Pow2.roundToPowerOfTwo(Math.max(8, bufferSize)); // lookahead doesn't work with capacity < 8 + int mask = p2capacity - 1; + AtomicReferenceArray buffer = new AtomicReferenceArray(p2capacity + 1); + producerBuffer = buffer; + producerMask = mask; + adjustLookAheadStep(p2capacity); + consumerBuffer = buffer; + consumerMask = mask; + producerLookAhead = mask - 1; // we know it's all empty to start with + soProducerIndex(0L); + } + + /** + * {@inheritDoc} + *

    + * This implementation is correct for single producer thread use only. + */ + @Override + public final boolean offer(final T e) { + if (e == null) { + throw new NullPointerException(); + } + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = producerBuffer; + final long index = lpProducerIndex(); + final int mask = producerMask; + final int offset = calcWrappedOffset(index, mask); + if (index < producerLookAhead) { + return writeToQueue(buffer, e, index, offset); + } else { + final int lookAheadStep = producerLookAheadStep; + // go around the buffer or resize if full (unless we hit max capacity) + int lookAheadElementOffset = calcWrappedOffset(index + lookAheadStep, mask); + if (null == lvElement(buffer, lookAheadElementOffset)) {// LoadLoad + producerLookAhead = index + lookAheadStep - 1; // joy, there's plenty of room + return writeToQueue(buffer, e, index, offset); + } else if (null != lvElement(buffer, calcWrappedOffset(index + 1, mask))) { // buffer is not full + return writeToQueue(buffer, e, index, offset); + } else { + resize(buffer, index, offset, e, mask); // add a buffer and link old to new + return true; + } + } + } + + private boolean writeToQueue(final AtomicReferenceArray buffer, final T e, final long index, final int offset) { + soProducerIndex(index + 1);// this ensures atomic write of long on 32bit platforms + soElement(buffer, offset, e);// StoreStore + return true; + } + + private void resize(final AtomicReferenceArray oldBuffer, final long currIndex, final int offset, final T e, + final long mask) { + final int capacity = oldBuffer.length(); + final AtomicReferenceArray newBuffer = new AtomicReferenceArray(capacity); + producerBuffer = newBuffer; + producerLookAhead = currIndex + mask - 1; + soProducerIndex(currIndex + 1);// this ensures correctness on 32bit platforms + soElement(newBuffer, offset, e);// StoreStore + soNext(oldBuffer, newBuffer); + soElement(oldBuffer, offset, HAS_NEXT); // new buffer is visible after element is + // inserted + } + + private void soNext(AtomicReferenceArray curr, AtomicReferenceArray next) { + soElement(curr, calcDirectOffset(curr.length() - 1), next); + } + @SuppressWarnings("unchecked") + private AtomicReferenceArray lvNext(AtomicReferenceArray curr) { + return (AtomicReferenceArray)lvElement(curr, calcDirectOffset(curr.length() - 1)); + } + /** + * {@inheritDoc} + *

    + * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final T poll() { + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + boolean isNextBuffer = e == HAS_NEXT; + if (null != e && !isNextBuffer) { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(buffer, offset, null);// StoreStore + return (T) e; + } else if (isNextBuffer) { + return newBufferPoll(lvNext(buffer), index, mask); + } + + return null; + } + + @SuppressWarnings("unchecked") + private T newBufferPoll(AtomicReferenceArray nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + final T n = (T) lvElement(nextBuffer, offsetInNew);// LoadLoad + if (null == n) { + return null; + } else { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(nextBuffer, offsetInNew, null);// StoreStore + return n; + } + } + + /** + * {@inheritDoc} + *

    + * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final T peek() { + final AtomicReferenceArray buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + if (e == HAS_NEXT) { + return newBufferPeek(lvNext(buffer), index, mask); + } + + return (T) e; + } + + @Override + public void clear() { + while (poll() != null || !isEmpty()); + } + + @SuppressWarnings("unchecked") + private T newBufferPeek(AtomicReferenceArray nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + return (T) lvElement(nextBuffer, offsetInNew);// LoadLoad + } + + @Override + public final int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + @Override + public boolean isEmpty() { + return lvProducerIndex() == lvConsumerIndex(); + } + + private void adjustLookAheadStep(int capacity) { + producerLookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + private long lvProducerIndex() { + return producerIndex; + } + + private long lvConsumerIndex() { + return consumerIndex; + } + + private long lpProducerIndex() { + return producerIndex; + } + + private long lpConsumerIndex() { + return consumerIndex; + } + + private void soProducerIndex(long v) { + PRODUCER_INDEX.lazySet(this, v); + } + + private void soConsumerIndex(long v) { + CONSUMER_INDEX.lazySet(this, v); + } + + private static final int calcWrappedOffset(long index, int mask) { + return calcDirectOffset((int)index & mask); + } + private static final int calcDirectOffset(int index) { + return index; + } + private static final void soElement(AtomicReferenceArray buffer, int offset, Object e) { + buffer.lazySet(offset, e); + } + + private static final Object lvElement(AtomicReferenceArray buffer, int offset) { + return buffer.get(offset); + } + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public E[] toArray(E[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(T e) { + throw new UnsupportedOperationException(); + } + + @Override + public T remove() { + throw new UnsupportedOperationException(); + } + + @Override + public T element() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/rx/internal/util/unsafe/QueueProgressIndicators.java b/src/main/java/rx/internal/util/unsafe/QueueProgressIndicators.java new file mode 100644 index 0000000000..185f0bd612 --- /dev/null +++ b/src/main/java/rx/internal/util/unsafe/QueueProgressIndicators.java @@ -0,0 +1,54 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/QueueProgressIndicators.java + */ +package rx.internal.util.unsafe; + +/** + * This interface is provided for monitoring purposes only and is only available on queues where it is easy to + * provide it. The producer/consumer progress indicators usually correspond with the number of elements + * offered/polled, but they are not guaranteed to maintain that semantic. + * + * @author nitsanw + * + */ +public interface QueueProgressIndicators { + + /** + * This method has no concurrent visibility semantics. The value returned may be negative. Under normal + * circumstances 2 consecutive calls to this method can offer an idea of progress made by producer threads + * by subtracting the 2 results though in extreme cases (if producers have progressed by more than 2^64) + * this may also fail.
    + * This value will normally indicate number of elements passed into the queue, but may under some + * circumstances be a derivative of that figure. This method should not be used to derive size or + * emptiness. + * + * @return the current value of the producer progress index + */ + public long currentProducerIndex(); + + /** + * This method has no concurrent visibility semantics. The value returned may be negative. Under normal + * circumstances 2 consecutive calls to this method can offer an idea of progress made by consumer threads + * by subtracting the 2 results though in extreme cases (if consumers have progressed by more than 2^64) + * this may also fail.
    + * This value will normally indicate number of elements taken out of the queue, but may under some + * circumstances be a derivative of that figure. This method should not be used to derive size or + * emptiness. + * + * @return the current value of the consumer progress index + */ + public long currentConsumerIndex(); +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java new file mode 100644 index 0000000000..c579864549 --- /dev/null +++ b/src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java @@ -0,0 +1,290 @@ +/* + * 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. + * + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/SpscUnboundedArrayQueue.java + */ +package rx.internal.util.unsafe; + +import static rx.internal.util.unsafe.UnsafeAccess.*; + +import java.lang.reflect.Field; +import java.util.AbstractQueue; +import java.util.Iterator; + +abstract class SpscUnboundedArrayQueueProducerFields extends AbstractQueue { + protected long producerIndex; +} + +abstract class SpscUnboundedArrayQueueProducerColdFields extends SpscUnboundedArrayQueueProducerFields { + protected int producerLookAheadStep; + protected long producerLookAhead; + protected long producerMask; + protected E[] producerBuffer; +} + +abstract class SpscUnboundedArrayQueueL2Pad extends SpscUnboundedArrayQueueProducerColdFields { + long p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12; +} + +abstract class SpscUnboundedArrayQueueConsumerColdField extends SpscUnboundedArrayQueueL2Pad { + protected long consumerMask; + protected E[] consumerBuffer; +} + +abstract class SpscUnboundedArrayQueueConsumerField extends SpscUnboundedArrayQueueConsumerColdField { + protected long consumerIndex; +} + +public class SpscUnboundedArrayQueue extends SpscUnboundedArrayQueueConsumerField + implements QueueProgressIndicators{ + static final int MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); + private final static long P_INDEX_OFFSET; + private final static long C_INDEX_OFFSET; + private static final long REF_ARRAY_BASE; + private static final int REF_ELEMENT_SHIFT; + private static final Object HAS_NEXT = new Object(); + static { + final int scale = UnsafeAccess.UNSAFE.arrayIndexScale(Object[].class); + if (4 == scale) { + REF_ELEMENT_SHIFT = 2; + } else if (8 == scale) { + REF_ELEMENT_SHIFT = 3; + } else { + throw new IllegalStateException("Unknown pointer size"); + } + // Including the buffer pad in the array base offset + REF_ARRAY_BASE = UnsafeAccess.UNSAFE.arrayBaseOffset(Object[].class); + try { + Field iField = SpscUnboundedArrayQueueProducerFields.class.getDeclaredField("producerIndex"); + P_INDEX_OFFSET = UNSAFE.objectFieldOffset(iField); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + try { + Field iField = SpscUnboundedArrayQueueConsumerField.class.getDeclaredField("consumerIndex"); + C_INDEX_OFFSET = UNSAFE.objectFieldOffset(iField); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + public SpscUnboundedArrayQueue(final int bufferSize) { + int p2capacity = Pow2.roundToPowerOfTwo(bufferSize); + long mask = p2capacity - 1; + E[] buffer = (E[]) new Object[p2capacity + 1]; + producerBuffer = buffer; + producerMask = mask; + adjustLookAheadStep(p2capacity); + consumerBuffer = buffer; + consumerMask = mask; + producerLookAhead = mask - 1; // we know it's all empty to start with + soProducerIndex(0l); + } + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + *

    + * This implementation is correct for single producer thread use only. + */ + @Override + public final boolean offer(final E e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + // local load of field to avoid repeated loads after volatile reads + final E[] buffer = producerBuffer; + final long index = producerIndex; + final long mask = producerMask; + final long offset = calcWrappedOffset(index, mask); + if (index < producerLookAhead) { + return writeToQueue(buffer, e, index, offset); + } else { + final int lookAheadStep = producerLookAheadStep; + // go around the buffer or resize if full (unless we hit max capacity) + long lookAheadElementOffset = calcWrappedOffset(index + lookAheadStep, mask); + if (null == lvElement(buffer, lookAheadElementOffset)) {// LoadLoad + producerLookAhead = index + lookAheadStep - 1; // joy, there's plenty of room + return writeToQueue(buffer, e, index, offset); + } else if (null != lvElement(buffer, calcWrappedOffset(index + 1, mask))) { // buffer is not full + return writeToQueue(buffer, e, index, offset); + } else { + resize(buffer, index, offset, e, mask); // add a buffer and link old to new + return true; + } + } + } + + private boolean writeToQueue(final E[] buffer, final E e, final long index, final long offset) { + soProducerIndex(index + 1);// this ensures atomic write of long on 32bit platforms + soElement(buffer, offset, e);// StoreStore + return true; + } + + @SuppressWarnings("unchecked") + private void resize(final E[] oldBuffer, final long currIndex, final long offset, final E e, + final long mask) { + final int capacity = oldBuffer.length; + final E[] newBuffer = (E[]) new Object[capacity]; + producerBuffer = newBuffer; + producerLookAhead = currIndex + mask - 1; + soProducerIndex(currIndex + 1);// this ensures correctness on 32bit platforms + soElement(newBuffer, offset, e);// StoreStore + soNext(oldBuffer, newBuffer); + soElement(oldBuffer, offset, HAS_NEXT); // new buffer is visible after element is + // inserted + } + + private void soNext(E[] curr, E[] next) { + soElement(curr, calcDirectOffset(curr.length -1), next); + } + @SuppressWarnings("unchecked") + private E[] lvNext(E[] curr) { + return (E[]) lvElement(curr, calcDirectOffset(curr.length -1)); + } + /** + * {@inheritDoc} + *

    + * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final E poll() { + // local load of field to avoid repeated loads after volatile reads + final E[] buffer = consumerBuffer; + final long index = consumerIndex; + final long mask = consumerMask; + final long offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + boolean isNextBuffer = e == HAS_NEXT; + if (null != e && !isNextBuffer) { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(buffer, offset, null);// StoreStore + return (E) e; + } else if (isNextBuffer) { + return newBufferPoll(lvNext(buffer), index, mask); + } + + return null; + } + + @SuppressWarnings("unchecked") + private E newBufferPoll(E[] nextBuffer, final long index, final long mask) { + consumerBuffer = nextBuffer; + final long offsetInNew = calcWrappedOffset(index, mask); + final E n = (E) lvElement(nextBuffer, offsetInNew);// LoadLoad + if (null == n) { + return null; + } else { + soConsumerIndex(index + 1);// this ensures correctness on 32bit platforms + soElement(nextBuffer, offsetInNew, null);// StoreStore + return n; + } + } + + /** + * {@inheritDoc} + *

    + * This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public final E peek() { + final E[] buffer = consumerBuffer; + final long index = consumerIndex; + final long mask = consumerMask; + final long offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset);// LoadLoad + if (e == HAS_NEXT) { + return newBufferPeek(lvNext(buffer), index, mask); + } + + return (E) e; + } + + @SuppressWarnings("unchecked") + private E newBufferPeek(E[] nextBuffer, final long index, final long mask) { + consumerBuffer = nextBuffer; + final long offsetInNew = calcWrappedOffset(index, mask); + return (E) lvElement(nextBuffer, offsetInNew);// LoadLoad + } + + @Override + public final int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + private void adjustLookAheadStep(int capacity) { + producerLookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + private long lvProducerIndex() { + return UNSAFE.getLongVolatile(this, P_INDEX_OFFSET); + } + + private long lvConsumerIndex() { + return UNSAFE.getLongVolatile(this, C_INDEX_OFFSET); + } + + private void soProducerIndex(long v) { + UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, v); + } + + private void soConsumerIndex(long v) { + UNSAFE.putOrderedLong(this, C_INDEX_OFFSET, v); + } + + private static final long calcWrappedOffset(long index, long mask) { + return calcDirectOffset(index & mask); + } + private static final long calcDirectOffset(long index) { + return REF_ARRAY_BASE + (index << REF_ELEMENT_SHIFT); + } + private static final void soElement(Object[] buffer, long offset, Object e) { + UNSAFE.putOrderedObject(buffer, offset, e); + } + + private static final Object lvElement(E[] buffer, long offset) { + return UNSAFE.getObjectVolatile(buffer, offset); + } + + @Override + public long currentProducerIndex() { + return lvProducerIndex(); + } + + @Override + public long currentConsumerIndex() { + return lvConsumerIndex(); + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/BufferUntilSubscriberTest.java b/src/test/java/rx/internal/operators/BufferUntilSubscriberTest.java index 50be759581..801138d4ab 100644 --- a/src/test/java/rx/internal/operators/BufferUntilSubscriberTest.java +++ b/src/test/java/rx/internal/operators/BufferUntilSubscriberTest.java @@ -15,20 +15,19 @@ */ package rx.internal.operators; -import org.junit.Assert; -import org.junit.Test; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + import rx.Observable; -import rx.functions.Action1; -import rx.functions.Func1; +import rx.exceptions.TestException; +import rx.functions.*; +import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - public class BufferUntilSubscriberTest { @Test @@ -82,4 +81,98 @@ public void call(List integers) { else Assert.assertEquals(NITERS, counter.get()); } -} + + @Test + public void testBackpressure() { + UnicastSubject bus = UnicastSubject.create(); + for (int i = 0; i < 32; i++) { + bus.onNext(i); + } + + TestSubscriber ts = TestSubscriber.create(0); + + bus.subscribe(ts); + + ts.assertValueCount(0); + ts.assertNoTerminalEvent(); + + ts.requestMore(10); + + ts.assertValueCount(10); + + ts.requestMore(22); + ts.assertValueCount(32); + + Assert.assertFalse(bus.state.caughtUp); + + ts.requestMore(Long.MAX_VALUE); + + Assert.assertTrue(bus.state.caughtUp); + + for (int i = 32; i < 64; i++) { + bus.onNext(i); + } + bus.onCompleted(); + + ts.assertValueCount(64); + ts.assertNoErrors(); + ts.assertCompleted(); + } + @Test + public void testErrorCutsAhead() { + UnicastSubject bus = UnicastSubject.create(); + for (int i = 0; i < 32; i++) { + bus.onNext(i); + } + bus.onError(new TestException()); + + TestSubscriber ts = TestSubscriber.create(0); + + bus.subscribe(ts); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } + @Test + public void testErrorCutsAheadAfterSubscribed() { + UnicastSubject bus = UnicastSubject.create(); + for (int i = 0; i < 32; i++) { + bus.onNext(i); + } + + TestSubscriber ts = TestSubscriber.create(0); + + bus.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoTerminalEvent(); + + bus.onError(new TestException()); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } + @Test + public void testUnsubscribeClearsQueue() { + UnicastSubject bus = UnicastSubject.create(); + for (int i = 0; i < 32; i++) { + bus.onNext(i); + } + + TestSubscriber ts = TestSubscriber.create(0); + ts.unsubscribe(); + + bus.subscribe(ts); + + ts.assertNoTerminalEvent(); + ts.assertNoValues(); + + Assert.assertTrue(bus.state.queue.isEmpty()); + + bus.onNext(32); + + Assert.assertTrue(bus.state.queue.isEmpty()); + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index c04b6dd910..d6a43525b2 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -728,7 +728,7 @@ public Observable call(Integer t) { Observable observable = Observable.just(t) .subscribeOn(sch) ; - Subject subject = BufferUntilSubscriber.create(); + Subject subject = UnicastSubject.create(); observable.subscribe(subject); return subject; } @@ -822,4 +822,4 @@ public Observable call(Integer t) { } } -} +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java b/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java index 05488379c2..9d6ee60baa 100644 --- a/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorWindowWithObservableTest.java @@ -383,7 +383,7 @@ public Observable call() { ts.assertValueCount(1); } @Test - public void testNoBackpressure() { + public void testInnerBackpressure() { Observable source = Observable.range(1, 10); final PublishSubject boundary = PublishSubject.create(); Func0> boundaryFunc = new Func0>() { @@ -409,7 +409,13 @@ public void onNext(Observable t) { ts1.assertValueCount(1); ts.assertNoErrors(); + ts.assertNotCompleted(); + ts.assertValues(1); + + ts.requestMore(11); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts.assertNoErrors(); ts.assertCompleted(); } @Test From 6caf9c6100dd7e94ed9fa19fa45fbe3482c2d87a Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 26 Nov 2015 08:57:08 +0100 Subject: [PATCH 452/857] 1.x: GroupBy backpressure fix --- .../internal/operators/OperatorGroupBy.java | 792 ++++++++++-------- .../operators/OperatorGroupByTest.java | 150 +++- .../internal/operators/OperatorRetryTest.java | 1 + 3 files changed, 579 insertions(+), 364 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorGroupBy.java b/src/main/java/rx/internal/operators/OperatorGroupBy.java index 02efb20f3f..38edc0a68f 100644 --- a/src/main/java/rx/internal/operators/OperatorGroupBy.java +++ b/src/main/java/rx/internal/operators/OperatorGroupBy.java @@ -15,25 +15,17 @@ */ package rx.internal.operators; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; - -import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Observable.Operator; -import rx.exceptions.*; -import rx.Observer; -import rx.Producer; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Func1; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import rx.*; +import rx.Observable.*; +import rx.functions.*; +import rx.internal.producers.ProducerArbiter; +import rx.internal.util.*; import rx.observables.GroupedObservable; -import rx.subjects.Subject; +import rx.plugins.RxJavaPlugins; import rx.subscriptions.Subscriptions; /** @@ -49,381 +41,523 @@ * @param * the value type of the groups */ -public class OperatorGroupBy implements Operator, T> { +public final class OperatorGroupBy implements Operator, T>{ final Func1 keySelector; - final Func1 valueSelector; + final Func1 valueSelector; + final int bufferSize; + final boolean delayError; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public OperatorGroupBy(Func1 keySelector) { + this(keySelector, (Func1)UtilityFunctions.identity(), RxRingBuffer.SIZE, false); + } - @SuppressWarnings("unchecked") - public OperatorGroupBy(final Func1 keySelector) { - this(keySelector, (Func1) IDENTITY); + public OperatorGroupBy(Func1 keySelector, Func1 valueSelector) { + this(keySelector, valueSelector, RxRingBuffer.SIZE, false); } - public OperatorGroupBy( - Func1 keySelector, - Func1 valueSelector) { + public OperatorGroupBy(Func1 keySelector, Func1 valueSelector, int bufferSize, boolean delayError) { this.keySelector = keySelector; this.valueSelector = valueSelector; + this.bufferSize = bufferSize; + this.delayError = delayError; } - + @Override - public Subscriber call(final Subscriber> child) { - return new GroupBySubscriber(keySelector, valueSelector, child); - } - - static final class GroupBySubscriber extends Subscriber { - private static final int MAX_QUEUE_SIZE = 1024; - final GroupBySubscriber self = this; - final Func1 keySelector; - final Func1 elementSelector; - final Subscriber> child; + public Subscriber call(Subscriber> t) { + final GroupBySubscriber parent = new GroupBySubscriber(t, keySelector, valueSelector, bufferSize, delayError); - // We should not call `unsubscribe()` until `groups.isEmpty() && child.isUnsubscribed()` is true. - // Use `WIP_FOR_UNSUBSCRIBE_UPDATER` to monitor these statuses and call `unsubscribe()` properly. - // Should check both when `child.unsubscribe` is called and any group is removed. - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP_FOR_UNSUBSCRIBE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "wipForUnsubscribe"); - volatile int wipForUnsubscribe = 1; - - public GroupBySubscriber( - Func1 keySelector, - Func1 elementSelector, - Subscriber> child) { - super(); - this.keySelector = keySelector; - this.elementSelector = elementSelector; - this.child = child; - child.add(Subscriptions.create(new Action0() { - - @Override - public void call() { - if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(self) == 0) { - self.unsubscribe(); - } - } - - })); - } - - private static class GroupState { - private final Subject s = BufferUntilSubscriber.create(); - private final AtomicLong requested = new AtomicLong(); - private final AtomicLong count = new AtomicLong(); - private final Queue buffer = new ConcurrentLinkedQueue(); // TODO should this be lazily created? - - public Observable getObservable() { - return s; + t.add(Subscriptions.create(new Action0() { + @Override + public void call() { + parent.cancel(); } + })); - public Observer getObserver() { - return s; - } + t.setProducer(parent.producer); + + return parent; + } + public static final class GroupByProducer implements Producer { + final GroupBySubscriber parent; + + public GroupByProducer(GroupBySubscriber parent) { + this.parent = parent; } - - private final ConcurrentHashMap> groups = new ConcurrentHashMap>(); - - private static final NotificationLite nl = NotificationLite.instance(); - - volatile int completionEmitted; - - private static final int UNTERMINATED = 0; - private static final int TERMINATED_WITH_COMPLETED = 1; - private static final int TERMINATED_WITH_ERROR = 2; - - // Must be one of `UNTERMINATED`, `TERMINATED_WITH_COMPLETED`, `TERMINATED_WITH_ERROR` - volatile int terminated = UNTERMINATED; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater COMPLETION_EMITTED_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "completionEmitted"); + @Override + public void request(long n) { + parent.requestMore(n); + } + } + + public static final class GroupBySubscriber + extends Subscriber { + final Subscriber> actual; + final Func1 keySelector; + final Func1 valueSelector; + final int bufferSize; + final boolean delayError; + final Map> groups; + final Queue> queue; + final GroupByProducer producer; + + static final Object NULL_KEY = new Object(); + + final ProducerArbiter s; + + volatile int cancelled; @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater TERMINATED_UPDATER = AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "terminated"); + static final AtomicIntegerFieldUpdater CANCELLED = + AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "cancelled"); volatile long requested; @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "requested"); - - volatile long bufferedCount; + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "requested"); + + volatile int groupCount; @SuppressWarnings("rawtypes") - static final AtomicLongFieldUpdater BUFFERED_COUNT = AtomicLongFieldUpdater.newUpdater(GroupBySubscriber.class, "bufferedCount"); + static final AtomicIntegerFieldUpdater GROUP_COUNT = + AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "groupCount"); + + Throwable error; + volatile boolean done; + volatile int wip; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(GroupBySubscriber.class, "wip"); + + public GroupBySubscriber(Subscriber> actual, Func1 keySelector, Func1 valueSelector, int bufferSize, boolean delayError) { + this.actual = actual; + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.bufferSize = bufferSize; + this.delayError = delayError; + this.groups = new ConcurrentHashMap>(); + this.queue = new ConcurrentLinkedQueue>(); + GROUP_COUNT.lazySet(this, 1); + this.s = new ProducerArbiter(); + this.s.request(bufferSize); + this.producer = new GroupByProducer(this); + } + @Override - public void onStart() { - REQUESTED.set(this, MAX_QUEUE_SIZE); - request(MAX_QUEUE_SIZE); + public void setProducer(Producer s) { + this.s.setProducer(s); } - + @Override - public void onCompleted() { - if (TERMINATED_UPDATER.compareAndSet(this, UNTERMINATED, TERMINATED_WITH_COMPLETED)) { - // if we receive onCompleted from our parent we onComplete children - // for each group check if it is ready to accept more events if so pass the oncomplete through else buffer it. - for (GroupState group : groups.values()) { - emitItem(group, nl.completed()); - } - - // special case (no groups emitted ... or all unsubscribed) - if (groups.isEmpty()) { - // we must track 'completionEmitted' seperately from 'completed' since `completeInner` can result in childObserver.onCompleted() being emitted - if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { - child.onCompleted(); - } - } + public void onNext(T t) { + if (done) { + return; } - } - @Override - public void onError(Throwable e) { - if (TERMINATED_UPDATER.compareAndSet(this, UNTERMINATED, TERMINATED_WITH_ERROR)) { - // It's safe to access all groups and emit the error. - // onNext and onError are in sequence so no group will be created in the loop. - for (GroupState group : groups.values()) { - emitItem(group, nl.error(e)); - } - try { - // we immediately tear everything down if we receive an error - child.onError(e); - } finally { - // We have not chained the subscribers, so need to call it explicitly. - unsubscribe(); + final Queue> q = this.queue; + final Subscriber> a = this.actual; + + K key; + try { + key = keySelector.call(t); + } catch (Throwable ex) { + unsubscribe(); + errorAll(a, q, ex); + return; + } + + boolean notNew = true; + Object mapKey = key != null ? key : NULL_KEY; + GroupedUnicast group = groups.get(mapKey); + if (group == null) { + // if the main has been cancelled, stop creating groups + // and skip this value + if (cancelled == 0) { + group = GroupedUnicast.createWith(key, bufferSize, this, delayError); + groups.put(mapKey, group); + + GROUP_COUNT.getAndIncrement(this); + + notNew = false; + q.offer(group); + drain(); + } else { + return; } } - } + + V v; + try { + v = valueSelector.call(t); + } catch (Throwable ex) { + unsubscribe(); + errorAll(a, q, ex); + return; + } - // The grouped observable propagates the 'producer.request' call from it's subscriber to this method - // Here we keep track of the requested count for each group - // If we already have items queued when a request comes in we vend those and decrement the outstanding request count + group.onNext(v); - void requestFromGroupedObservable(long n, GroupState group) { - BackpressureUtils.getAndAddRequest(group.requested, n); - if (group.count.getAndIncrement() == 0) { - pollQueue(group); + if (notNew) { + s.request(1); } } - - private Object groupedKey(K key) { - return key == null ? NULL_KEY : key; + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(t); + return; + } + error = t; + done = true; + GROUP_COUNT.decrementAndGet(this); + drain(); } - - @SuppressWarnings("unchecked") - private K getKey(Object groupedKey) { - return groupedKey == NULL_KEY ? null : (K) groupedKey; + + @Override + public void onCompleted() { + if (done) { + return; + } + done = true; + GROUP_COUNT.decrementAndGet(this); + drain(); } - @Override - public void onNext(T t) { - try { - final Object key = groupedKey(keySelector.call(t)); - GroupState group = groups.get(key); - if (group == null) { - // this group doesn't exist - if (child.isUnsubscribed()) { - // we have been unsubscribed on the outer so won't send any more groups - return; - } - group = createNewGroup(key); + public void requestMore(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } + + BackpressureUtils.getAndAddRequest(REQUESTED, this, n); + drain(); + } + + public void cancel() { + // cancelling the main source means we don't want any more groups + // but running groups still require new values + if (CANCELLED.compareAndSet(this, 0, 1)) { + if (GROUP_COUNT.decrementAndGet(this) == 0) { + unsubscribe(); } - if (group != null) { - emitItem(group, nl.next(t)); + } + } + + public void cancel(K key) { + Object mapKey = key != null ? key : NULL_KEY; + if (groups.remove(mapKey) != null) { + if (GROUP_COUNT.decrementAndGet(this) == 0) { + unsubscribe(); } - } catch (Throwable e) { - Exceptions.throwOrReport(e, this, t); } } - - private GroupState createNewGroup(final Object key) { - final GroupState groupState = new GroupState(); - - GroupedObservable go = GroupedObservable.create(getKey(key), new OnSubscribe() { - - @Override - public void call(final Subscriber o) { - o.setProducer(new Producer() { - - @Override - public void request(long n) { - requestFromGroupedObservable(n, groupState); - } - - }); - - final AtomicBoolean once = new AtomicBoolean(); - - groupState.getObservable().doOnUnsubscribe(new Action0() { - - @Override - public void call() { - if (once.compareAndSet(false, true)) { - // done once per instance, either onComplete or onUnSubscribe - cleanupGroup(key); - } - } - - }).unsafeSubscribe(new Subscriber(o) { - @Override - public void onCompleted() { - o.onCompleted(); - // eagerly cleanup instead of waiting for unsubscribe - if (once.compareAndSet(false, true)) { - // done once per instance, either onComplete or onUnSubscribe - cleanupGroup(key); - } - } - - @Override - public void onError(Throwable e) { - o.onError(e); - // eagerly cleanup instead of waiting for unsubscribe - if (once.compareAndSet(false, true)) { - // done once per instance, either onComplete or onUnSubscribe - cleanupGroup(key); - } - } - - @Override - public void onNext(T t) { - try { - o.onNext(elementSelector.call(t)); - } catch (Throwable e) { - Exceptions.throwOrReport(e, this, t); - } - } - }); + + void drain() { + if (WIP.getAndIncrement(this) != 0) { + return; + } + + int missed = 1; + + final Queue> q = this.queue; + final Subscriber> a = this.actual; + + for (;;) { + + if (checkTerminated(done, q.isEmpty(), a, q)) { + return; } - }); + + long r = requested; + boolean unbounded = r == Long.MAX_VALUE; + long e = 0L; + + while (r != 0) { + boolean d = done; + + GroupedObservable t = q.poll(); + + boolean empty = t == null; + + if (checkTerminated(d, empty, a, q)) { + return; + } + + if (empty) { + break; + } - GroupState putIfAbsent; - for (;;) { - int wip = wipForUnsubscribe; - if (wip <= 0) { - return null; + a.onNext(t); + + r--; + e--; + } + + if (e != 0L) { + if (!unbounded) { + REQUESTED.addAndGet(this, e); + } + s.request(-e); } - if (WIP_FOR_UNSUBSCRIBE_UPDATER.compareAndSet(this, wip, wip + 1)) { - putIfAbsent = groups.putIfAbsent(key, groupState); + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { break; } } - if (putIfAbsent != null) { - // this shouldn't happen (because we receive onNext sequentially) and would mean we have a bug - throw new IllegalStateException("Group already existed while creating a new one"); + } + + void errorAll(Subscriber> a, Queue q, Throwable ex) { + q.clear(); + List> list = new ArrayList>(groups.values()); + groups.clear(); + + for (GroupedUnicast e : list) { + e.onError(ex); } - child.onNext(go); - - return groupState; + + a.onError(ex); } - - private void cleanupGroup(Object key) { - GroupState removed; - removed = groups.remove(key); - if (removed != null) { - if (!removed.buffer.isEmpty()) { - BUFFERED_COUNT.addAndGet(self, -removed.buffer.size()); + + boolean checkTerminated(boolean d, boolean empty, + Subscriber> a, Queue q) { + if (d) { + Throwable err = error; + if (err != null) { + errorAll(a, q, err); + return true; + } else + if (empty) { + List> list = new ArrayList>(groups.values()); + groups.clear(); + + for (GroupedUnicast e : list) { + e.onComplete(); + } + + actual.onCompleted(); + return true; } - completeInner(); - // since we may have unsubscribed early with items in the buffer - // we remove those above and have freed up room to request more - // so give it a chance to request more now - requestMoreIfNecessary(); } + return false; } + } + + static final class GroupedUnicast extends GroupedObservable { + + public static GroupedUnicast createWith(K key, int bufferSize, GroupBySubscriber parent, boolean delayError) { + State state = new State(bufferSize, parent, key, delayError); + return new GroupedUnicast(key, state); + } + + final State state; + + protected GroupedUnicast(K key, State state) { + super(key, state); + this.state = state; + } + + public void onNext(T t) { + state.onNext(t); + } + + public void onError(Throwable e) { + state.onError(e); + } + + public void onComplete() { + state.onComplete(); + } + } + + static final class State extends AtomicInteger implements Producer, Subscription, OnSubscribe { + /** */ + private static final long serialVersionUID = -3852313036005250360L; + + final K key; + final Queue queue; + final GroupBySubscriber parent; + final boolean delayError; + + volatile long requested; + @SuppressWarnings("rawtypes") + static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(State.class, "requested"); + + volatile boolean done; + Throwable error; + + volatile int cancelled; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater CANCELLED = + AtomicIntegerFieldUpdater.newUpdater(State.class, "cancelled"); + + volatile Subscriber actual; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater ACTUAL = + AtomicReferenceFieldUpdater.newUpdater(State.class, Subscriber.class, "actual"); - private void emitItem(GroupState groupState, Object item) { - Queue q = groupState.buffer; - AtomicLong keyRequested = groupState.requested; - //don't need to check for requested being Long.MAX_VALUE because this - //field is capped at MAX_QUEUE_SIZE - REQUESTED.decrementAndGet(this); - // short circuit buffering - if (keyRequested != null && keyRequested.get() > 0 && (q == null || q.isEmpty())) { - @SuppressWarnings("unchecked") - Observer obs = (Observer)groupState.getObserver(); - nl.accept(obs, item); - if (keyRequested.get() != Long.MAX_VALUE) { - // best endeavours check (no CAS loop here) because we mainly care about - // the initial request being Long.MAX_VALUE and that value being conserved. - keyRequested.decrementAndGet(); - } - } else { - q.add(item); - BUFFERED_COUNT.incrementAndGet(this); - - if (groupState.count.getAndIncrement() == 0) { - pollQueue(groupState); - } + volatile int once; + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(State.class, "once"); + + + public State(int bufferSize, GroupBySubscriber parent, K key, boolean delayError) { + this.queue = new ConcurrentLinkedQueue(); + this.parent = parent; + this.key = key; + this.delayError = delayError; + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= required but it was " + n); + } + if (n != 0L) { + BackpressureUtils.getAndAddRequest(REQUESTED, this, n); + drain(); } - requestMoreIfNecessary(); } - - private void pollQueue(GroupState groupState) { - do { - drainIfPossible(groupState); - long c = groupState.count.decrementAndGet(); - if (c > 1) { - - /* - * Set down to 1 and then iterate again. - * we lower it to 1 otherwise it could have grown very large while in the last poll loop - * and then we can end up looping all those times again here before existing even once we've drained - */ - groupState.count.set(1); - // we now loop again, and if anything tries scheduling again after this it will increment and cause us to loop again after + + @Override + public boolean isUnsubscribed() { + return cancelled != 0; + } + + @Override + public void unsubscribe() { + if (CANCELLED.compareAndSet(this, 0, 1)) { + if (getAndIncrement() == 0) { + parent.cancel(key); } - } while (groupState.count.get() > 0); + } + } + + @Override + public void call(Subscriber s) { + if (ONCE.compareAndSet(this, 0, 1)) { + s.add(this); + s.setProducer(this); + ACTUAL.lazySet(this, s); + drain(); + } else { + s.onError(new IllegalStateException("Only one Subscriber allowed!")); + } } - private void requestMoreIfNecessary() { - if (REQUESTED.get(this) == 0 && terminated == 0) { - long toRequest = MAX_QUEUE_SIZE - BUFFERED_COUNT.get(this); - if (toRequest > 0 && REQUESTED.compareAndSet(this, 0, toRequest)) { - request(toRequest); - } + public void onNext(T t) { + if (t == null) { + error = new NullPointerException(); + done = true; + } else { + queue.offer(NotificationLite.instance().next(t)); } + drain(); + } + + public void onError(Throwable e) { + error = e; + done = true; + drain(); + } + + public void onComplete() { + done = true; + drain(); } - private void drainIfPossible(GroupState groupState) { - while (groupState.requested.get() > 0) { - Object t = groupState.buffer.poll(); - if (t != null) { - @SuppressWarnings("unchecked") - Observer obs = (Observer)groupState.getObserver(); - nl.accept(obs, t); - if (groupState.requested.get()!=Long.MAX_VALUE) { - // best endeavours check (no CAS loop here) because we mainly care about - // the initial request being Long.MAX_VALUE and that value being conserved. - groupState.requested.decrementAndGet(); + void drain() { + if (getAndIncrement() != 0) { + return; + } + int missed = 1; + + final Queue q = queue; + final boolean delayError = this.delayError; + Subscriber a = actual; + NotificationLite nl = NotificationLite.instance(); + for (;;) { + if (a != null) { + if (checkTerminated(done, q.isEmpty(), a, delayError)) { + return; } - BUFFERED_COUNT.decrementAndGet(this); - - // if we have used up all the events we requested from upstream then figure out what to ask for this time based on the empty space in the buffer - requestMoreIfNecessary(); - } else { - // queue is empty break + + long r = requested; + boolean unbounded = r == Long.MAX_VALUE; + long e = 0; + + while (r != 0L) { + boolean d = done; + Object v = q.poll(); + boolean empty = v == null; + + if (checkTerminated(d, empty, a, delayError)) { + return; + } + + if (empty) { + break; + } + + a.onNext(nl.getValue(v)); + + r--; + e--; + } + + if (e != 0L) { + if (!unbounded) { + REQUESTED.addAndGet(this, e); + } + parent.s.request(-e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { break; } + if (a == null) { + a = actual; + } } } - - private void completeInner() { - // A group is removed, so check if we need to call `unsubscribe` - if (WIP_FOR_UNSUBSCRIBE_UPDATER.decrementAndGet(this) == 0) { - // It means `groups.isEmpty() && child.isUnsubscribed()` is true - unsubscribe(); - } else if (groups.isEmpty() && terminated == TERMINATED_WITH_COMPLETED) { - // if we have no outstanding groups (all completed or unsubscribe) and terminated on outer - // completionEmitted ensures we only emit onCompleted once - if (COMPLETION_EMITTED_UPDATER.compareAndSet(this, 0, 1)) { - child.onCompleted(); + + boolean checkTerminated(boolean d, boolean empty, Subscriber a, boolean delayError) { + if (cancelled != 0) { + queue.clear(); + parent.cancel(key); + return true; + } + + if (d) { + if (delayError) { + if (empty) { + Throwable e = error; + if (e != null) { + a.onError(e); + } else { + a.onCompleted(); + } + return true; + } + } else { + Throwable e = error; + if (e != null) { + queue.clear(); + a.onError(e); + return true; + } else + if (empty) { + a.onCompleted(); + return true; + } } } + + return false; } - } - - private final static Func1 IDENTITY = new Func1() { - @Override - public Object call(Object t) { - return t; - } - }; - - private static final Object NULL_KEY = new Object(); } diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index b14b7ad373..57cfdbcf4a 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -15,45 +15,24 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Matchers; -import org.mockito.MockitoAnnotations; - -import rx.Notification; +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.*; + +import org.junit.*; +import org.mockito.*; + +import rx.*; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observer; -import rx.Subscriber; -import rx.Subscription; import rx.exceptions.TestException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.internal.util.UtilityFunctions; +import rx.functions.*; +import rx.internal.util.*; import rx.observables.GroupedObservable; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -1501,4 +1480,105 @@ public void onNext(Integer t) { }}); assertTrue(completed.get()); } + + /** + * Issue #3425. + * + * The problem is that a request of 1 may create a new group, emit to the desired group + * or emit to a completely different group. In this test, the merge requests N which + * must be produced by the range, however it will create a bunch of groups before the actual + * group receives a value. + */ + @Test + public void testBackpressureObserveOnOuter() { + for (int j = 0; j < 1000; j++) { + Observable.merge( + Observable.range(0, 500) + .groupBy(new Func1() { + @Override + public Object call(Integer i) { + return i % (RxRingBuffer.SIZE + 2); + } + }) + .observeOn(Schedulers.computation()) + ).toBlocking().last(); + } + } + + /** + * Synchronous verification of issue #3425. + */ + @Test + public void testBackpressureInnerDoesntOverflowOuter() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1, 2) + .groupBy(new Func1() { + @Override + public Object call(Integer v) { + return v; + } + }) + .doOnNext(new Action1>() { + @Override + public void call(GroupedObservable g) { + // this will request Long.MAX_VALUE + g.subscribe(); + } + }) + // this won't request anything just yet + .subscribe(ts) + ; + ts.requestMore(1); + + ts.assertNotCompleted(); + ts.assertNoErrors(); + ts.assertValueCount(1); + } + + @Test + public void testOneGroupInnerRequestsTwiceBuffer() { + TestSubscriber ts1 = TestSubscriber.create(0); + final TestSubscriber ts2 = TestSubscriber.create(0); + + Observable.range(1, RxRingBuffer.SIZE * 2) + .groupBy(new Func1() { + @Override + public Object call(Integer v) { + return 1; + } + }) + .doOnNext(new Action1>() { + @Override + public void call(GroupedObservable g) { + g.subscribe(ts2); + } + }) + .subscribe(ts1); + + ts1.assertNoValues(); + ts1.assertNoErrors(); + ts1.assertNotCompleted(); + + ts2.assertNoValues(); + ts2.assertNoErrors(); + ts2.assertNotCompleted(); + + ts1.requestMore(1); + + ts1.assertValueCount(1); + ts1.assertNoErrors(); + ts1.assertNotCompleted(); + + ts2.assertNoValues(); + ts2.assertNoErrors(); + ts2.assertNotCompleted(); + + ts2.requestMore(RxRingBuffer.SIZE * 2); + + ts2.assertValueCount(RxRingBuffer.SIZE * 2); + ts2.assertNoErrors(); + ts2.assertNotCompleted(); + } + } diff --git a/src/test/java/rx/internal/operators/OperatorRetryTest.java b/src/test/java/rx/internal/operators/OperatorRetryTest.java index 146ee3c254..dc6eb510a9 100644 --- a/src/test/java/rx/internal/operators/OperatorRetryTest.java +++ b/src/test/java/rx/internal/operators/OperatorRetryTest.java @@ -874,6 +874,7 @@ public void call(Subscriber o) { }); origin.retry() + .onBackpressureBuffer() // FIXME the new GroupBy won't request enough for this particular test and retry overflows .groupBy(new Func1() { @Override public String call(String t1) { From e477c4f48198af26075a5efd76b6ceec7ec84230 Mon Sep 17 00:00:00 2001 From: Niklas Baudy Date: Sat, 28 Nov 2015 00:44:00 +0100 Subject: [PATCH 453/857] Remove double whitespace in if conditions --- .../java/rx/internal/operators/OperatorDebounceWithTime.java | 2 +- src/main/java/rx/observers/TestSubscriber.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java index df7c451287..f98639aff3 100644 --- a/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java +++ b/src/main/java/rx/internal/operators/OperatorDebounceWithTime.java @@ -163,7 +163,7 @@ public void emitAndComplete(Subscriber onNextAndComplete, Subscriber onErr emitting = true; } - if (localHasValue) { + if (localHasValue) { try { onNextAndComplete.onNext(localValue); } catch (Throwable e) { diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 2d46a25179..88ed459bfb 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -122,7 +122,7 @@ public static TestSubscriber create(Observer delegate) { @Override public void onStart() { - if (initialRequest >= 0) { + if (initialRequest >= 0) { requestMore(initialRequest); } } From 5f2467f3686703cf353fe93f9e96b01f256d36fb Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 1 Oct 2015 09:45:26 +0200 Subject: [PATCH 454/857] Public API changes for 1.1.0 release --- src/main/java/rx/Observable.java | 93 +-- src/main/java/rx/Single.java | 7 +- src/main/java/rx/SingleSubscriber.java | 5 +- src/main/java/rx/exceptions/Exceptions.java | 3 +- .../OperatorOnBackpressureBlock.java | 95 --- .../operators/OperatorTakeUntilPredicate.java | 2 - .../util/BackpressureDrainManager.java | 4 +- .../rx/observables/AbstractOnSubscribe.java | 622 ------------------ .../rx/observables/ConnectableObservable.java | 8 +- src/main/java/rx/observers/Subscribers.java | 10 +- .../java/rx/observers/TestSubscriber.java | 95 ++- .../java/rx/plugins/RxJavaErrorHandler.java | 9 +- src/main/java/rx/subjects/AsyncSubject.java | 40 +- .../java/rx/subjects/BehaviorSubject.java | 46 +- src/main/java/rx/subjects/PublishSubject.java | 56 +- src/main/java/rx/subjects/ReplaySubject.java | 44 +- .../java/rx/subjects/SerializedSubject.java | 72 -- src/main/java/rx/subjects/Subject.java | 118 +--- .../java/rx/subscriptions/Subscriptions.java | 4 +- .../rx/observables/SyncOnSubscribePerf.java | 24 - .../operators/OnBackpressureBlockTest.java | 347 ---------- .../internal/operators/OperatorScanTest.java | 15 +- .../observables/AbstractOnSubscribeTest.java | 540 --------------- .../java/rx/subjects/AsyncSubjectTest.java | 45 +- .../java/rx/subjects/BehaviorSubjectTest.java | 88 ++- .../java/rx/subjects/PublishSubjectTest.java | 40 +- .../java/rx/subjects/ReplaySubjectTest.java | 174 ++++- .../rx/subjects/SerializedSubjectTest.java | 376 +---------- 28 files changed, 508 insertions(+), 2474 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java delete mode 100644 src/main/java/rx/observables/AbstractOnSubscribe.java delete mode 100644 src/test/java/rx/internal/operators/OnBackpressureBlockTest.java delete mode 100644 src/test/java/rx/observables/AbstractOnSubscribeTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 19c5958005..3b9f404e08 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -227,7 +227,7 @@ public interface Transformer extends Func1, Observable> { * @see ReactiveX documentation: Single * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ - @Experimental + @Beta public Single toSingle() { return new Single(OnSubscribeSingle.create(this)); } @@ -1789,9 +1789,8 @@ public final static Observable merge(ObservableReactiveX operators documentation: Merge - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental @SuppressWarnings({"unchecked", "rawtypes"}) public final static Observable merge(Observable> source, int maxConcurrent) { if (source.getClass() == ScalarSynchronousObservable.class) { @@ -2088,9 +2087,8 @@ public final static Observable merge(Observable[] sequences) * the maximum number of Observables that may be subscribed to concurrently * @return an Observable that emits all of the items emitted by the Observables in the Array * @see ReactiveX operators documentation: Merge - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public final static Observable merge(Observable[] sequences, int maxConcurrent) { return merge(from(sequences), maxConcurrent); } @@ -4014,9 +4012,8 @@ public void call(Subscriber subscriber) { * the alternate Observable to subscribe to if the source does not emit any items * @return an Observable that emits the items emitted by the source Observable or the items of an * alternate Observable if the source Observable is empty. - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public final Observable switchIfEmpty(Observable alternate) { return lift(new OperatorSwitchIfEmpty(alternate)); } @@ -5896,9 +5893,8 @@ public final Observable onBackpressureBuffer() { * * @return the source 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) + * @since 1.1.0 */ - @Beta public final Observable onBackpressureBuffer(long capacity) { return lift(new OperatorOnBackpressureBuffer(capacity)); } @@ -5917,9 +5913,8 @@ public final Observable onBackpressureBuffer(long capacity) { * * @return the source 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) + * @since 1.1.0 */ - @Beta public final Observable onBackpressureBuffer(long capacity, Action0 onOverflow) { return lift(new OperatorOnBackpressureBuffer(capacity, onOverflow)); } @@ -5941,9 +5936,8 @@ public final Observable onBackpressureBuffer(long capacity, Action0 onOverflo * @return the source Observable modified to drop {@code onNext} notifications on overflow * @see ReactiveX operators documentation: backpressure operators * @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) + * @since 1.1.0 */ - @Experimental public final Observable onBackpressureDrop(Action1 onDrop) { return lift(new OperatorOnBackpressureDrop(onDrop)); } @@ -5968,72 +5962,6 @@ public final Observable onBackpressureDrop() { return lift(OperatorOnBackpressureDrop.instance()); } - /** - * Instructs an Observable that is emitting items faster than its observer can consume them to - * block the producer thread. - *

    - * - *

    - * The producer side can emit up to {@code maxQueueLength} onNext elements without blocking, but the - * consumer side considers the amount its downstream requested through {@code Producer.request(n)} - * and doesn't emit more than requested even if more is available. For example, using - * {@code onBackpressureBlock(384).observeOn(Schedulers.io())} will not throw a MissingBackpressureException. - *

    - * Note that if the upstream Observable does support backpressure, this operator ignores that capability - * and doesn't propagate any backpressure requests from downstream. - *

    - * Warning! Using a chain like {@code source.onBackpressureBlock().subscribeOn(scheduler)} is prone to - * deadlocks because the consumption of the internal queue is scheduled behind a blocked emission by - * the subscribeOn. In order to avoid this, the operators have to be swapped in the chain: - * {@code source.subscribeOn(scheduler).onBackpressureBlock()} and in general, no subscribeOn operator should follow - * this operator. - * - * @param maxQueueLength the maximum number of items the producer can emit without blocking - * @return the source Observable modified to block {@code onNext} notifications on overflow - * @see ReactiveX operators documentation: backpressure operators - * @Experimental The behavior of this can change at any time. - * @deprecated The operator doesn't work properly with {@link #subscribeOn(Scheduler)} and is prone to - * deadlocks. It will be removed/unavailable starting from 1.1. - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) - */ - @Experimental - @Deprecated - public final Observable onBackpressureBlock(int maxQueueLength) { - return lift(new OperatorOnBackpressureBlock(maxQueueLength)); - } - - /** - * Instructs an Observable that is emitting items faster than its observer can consume them to block the - * producer thread if the number of undelivered onNext events reaches the system-wide ring buffer size. - *

    - * - *

    - * The producer side can emit up to the system-wide ring buffer size onNext elements without blocking, but - * the consumer side considers the amount its downstream requested through {@code Producer.request(n)} - * and doesn't emit more than requested even if available. - *

    - * Note that if the upstream Observable does support backpressure, this operator ignores that capability - * and doesn't propagate any backpressure requests from downstream. - *

    - * Warning! Using a chain like {@code source.onBackpressureBlock().subscribeOn(scheduler)} is prone to - * deadlocks because the consumption of the internal queue is scheduled behind a blocked emission by - * the subscribeOn. In order to avoid this, the operators have to be swapped in the chain: - * {@code source.subscribeOn(scheduler).onBackpressureBlock()} and in general, no subscribeOn operator should follow - * this operator. - * - * @return the source Observable modified to block {@code onNext} notifications on overflow - * @see ReactiveX operators documentation: backpressure operators - * @Experimental The behavior of this can change at any time. - * @deprecated The operator doesn't work properly with {@link #subscribeOn(Scheduler)} and is prone to - * deadlocks. It will be removed/unavailable starting from 1.1. - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) - */ - @Experimental - @Deprecated - public final Observable onBackpressureBlock() { - return onBackpressureBlock(rx.internal.util.RxRingBuffer.SIZE); - } - /** * Instructs an Observable that is emitting items faster than its observer can consume them to * hold onto the latest value and emit that on request. @@ -6050,10 +5978,8 @@ public final Observable onBackpressureBlock() { * requesting more than 1 from downstream doesn't guarantee a continuous delivery of onNext events. * * @return the source Observable modified so that it emits the most recently-received item upon request - * @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) + * @since 1.1.0 */ - @Experimental public final Observable onBackpressureLatest() { return lift(OperatorOnBackpressureLatest.instance()); } @@ -8728,9 +8654,8 @@ public final Observable takeWhile(final Func1 predicate) * condition after each item, and then completes if the condition is satisfied. * @see ReactiveX operators documentation: TakeUntil * @see Observable#takeWhile(Func1) - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public final Observable takeUntil(final Func1 stopPredicate) { return lift(new OperatorTakeUntilPredicate(stopPredicate)); } diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index f862d42e0c..190e1630b3 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -40,11 +40,12 @@ import rx.internal.operators.OperatorSubscribeOn; import rx.internal.operators.OperatorTimeout; import rx.internal.operators.OperatorZip; + +import rx.annotations.Beta; import rx.internal.producers.SingleDelayedProducer; import rx.singles.BlockingSingle; import rx.observers.SafeSubscriber; -import rx.plugins.RxJavaObservableExecutionHook; -import rx.plugins.RxJavaPlugins; +import rx.plugins.*; import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; @@ -69,7 +70,7 @@ * the type of the item emitted by the Single * @since (If this class graduates from "Experimental" replace this parenthetical with the release number) */ -@Experimental +@Beta public class Single { final Observable.OnSubscribe onSubscribe; diff --git a/src/main/java/rx/SingleSubscriber.java b/src/main/java/rx/SingleSubscriber.java index 164933a1a3..7ab135e8ab 100644 --- a/src/main/java/rx/SingleSubscriber.java +++ b/src/main/java/rx/SingleSubscriber.java @@ -15,7 +15,7 @@ */ package rx; -import rx.annotations.Experimental; +import rx.annotations.Beta; import rx.internal.util.SubscriptionList; /** @@ -29,8 +29,9 @@ * @see ReactiveX documentation: Observable * @param * the type of item the SingleSubscriber expects to observe + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ -@Experimental +@Beta public abstract class SingleSubscriber implements Subscription { private final SubscriptionList cs = new SubscriptionList(); diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index 4701e2bb5f..dea54a9b26 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -152,9 +152,8 @@ public static Throwable getFinalCause(Throwable e) { * @param exceptions the collection of exceptions. If null or empty, no exception is thrown. * If the collection contains a single exception, that exception is either thrown as-is or wrapped into a * CompositeException. Multiple exceptions are wrapped into a CompositeException. - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public static void throwIfAny(List exceptions) { if (exceptions != null && !exceptions.isEmpty()) { if (exceptions.size() == 1) { diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java deleted file mode 100644 index 71a5fc4993..0000000000 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBlock.java +++ /dev/null @@ -1,95 +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.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; - -import rx.Observable.Operator; -import rx.Subscriber; -import rx.internal.util.BackpressureDrainManager; - -/** - * Operator that blocks the producer thread in case a backpressure is needed. - */ -public class OperatorOnBackpressureBlock implements Operator { - final int max; - public OperatorOnBackpressureBlock(int max) { - this.max = max; - } - @Override - public Subscriber call(Subscriber child) { - BlockingSubscriber s = new BlockingSubscriber(max, child); - s.init(); - return s; - } - - static final class BlockingSubscriber extends Subscriber implements BackpressureDrainManager.BackpressureQueueCallback { - final NotificationLite nl = NotificationLite.instance(); - final BlockingQueue queue; - final Subscriber child; - final BackpressureDrainManager manager; - public BlockingSubscriber(int max, Subscriber child) { - this.queue = new ArrayBlockingQueue(max); - this.child = child; - this.manager = new BackpressureDrainManager(this); - } - void init() { - child.add(this); - child.setProducer(manager); - } - @Override - public void onNext(T t) { - try { - queue.put(nl.next(t)); - manager.drain(); - } catch (InterruptedException ex) { - if (!isUnsubscribed()) { - onError(ex); - } - } - } - @Override - public void onError(Throwable e) { - manager.terminateAndDrain(e); - } - @Override - public void onCompleted() { - manager.terminateAndDrain(); - } - @Override - public boolean accept(Object value) { - return nl.accept(child, value); - } - @Override - public void complete(Throwable exception) { - if (exception != null) { - child.onError(exception); - } else { - child.onCompleted(); - } - } - @Override - public Object peek() { - return queue.peek(); - } - @Override - public Object poll() { - return queue.poll(); - } - } -} diff --git a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java index c33fab0b47..c65946bab1 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java @@ -17,7 +17,6 @@ import rx.*; import rx.Observable.Operator; -import rx.annotations.Experimental; import rx.exceptions.Exceptions; import rx.functions.Func1; @@ -26,7 +25,6 @@ * the provided predicate returns false *

    */ -@Experimental public final class OperatorTakeUntilPredicate implements Operator { /** Subscriber returned to the upstream. */ private final class ParentSubscriber extends Subscriber { diff --git a/src/main/java/rx/internal/util/BackpressureDrainManager.java b/src/main/java/rx/internal/util/BackpressureDrainManager.java index 38f714b67f..c90e9591df 100644 --- a/src/main/java/rx/internal/util/BackpressureDrainManager.java +++ b/src/main/java/rx/internal/util/BackpressureDrainManager.java @@ -23,7 +23,9 @@ /** * Manages the producer-backpressure-consumer interplay by * matching up available elements with requested elements and/or - * terminal events. + * terminal events. + * + * @since 1.1.0 */ @Experimental public final class BackpressureDrainManager extends AtomicLong implements Producer { diff --git a/src/main/java/rx/observables/AbstractOnSubscribe.java b/src/main/java/rx/observables/AbstractOnSubscribe.java deleted file mode 100644 index 6becdc50a3..0000000000 --- a/src/main/java/rx/observables/AbstractOnSubscribe.java +++ /dev/null @@ -1,622 +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.observables; - -import java.util.Arrays; -import java.util.concurrent.atomic.*; - -import rx.*; -import rx.Observable.OnSubscribe; -import rx.annotations.Experimental; -import rx.exceptions.CompositeException; -import rx.functions.*; -import rx.internal.operators.BackpressureUtils; - -/** - * Abstract base class for the {@link OnSubscribe} interface that helps you build Observable sources one - * {@code onNext} at a time, and automatically supports unsubscription and backpressure. - *

    - *

    Usage rules

    - * When you implement the {@code next()} method, you - *
      - *
    • should either - *
        - *
      • create the next value and signal it via {@link SubscriptionState#onNext state.onNext()},
      • - *
      • signal a terminal condition via {@link SubscriptionState#onError state.onError()}, or - * {@link SubscriptionState#onCompleted state.onCompleted()}, or
      • - *
      • signal a stop condition via {@link SubscriptionState#stop state.stop()} indicating no further values - * will be sent.
      • - *
      - *
    • - *
    • may - *
        - *
      • call {@link SubscriptionState#onNext state.onNext()} and either - * {@link SubscriptionState#onError state.onError()} or - * {@link SubscriptionState#onCompleted state.onCompleted()} together, and - *
      • block or sleep. - *
      - *
    • - *
    • should not - *
        - *
      • do nothing or do async work and not produce any event or request stopping. If neither of - * the methods are called, an {@link IllegalStateException} is forwarded to the {@code Subscriber} and - * the Observable is terminated;
      • - *
      • call the {@code state.on}foo() methods more than once (yields - * {@link IllegalStateException}).
      • - *
      - *
    • - *
    - * - * The {@link SubscriptionState} object features counters that may help implement a state machine: - *
      - *
    • A call counter, accessible via {@link SubscriptionState#calls state.calls()} tells how many times the - * {@code next()} was run (zero based).
    • - *
    • You can use a phase counter, accessible via {@link SubscriptionState#phase state.phase}, that helps track - * the current emission phase, in a {@code switch()} statement to implement the state machine. (It is named - * {@code phase} to avoid confusion with the per-subscriber state.)
    • - *
    • You can arbitrarily change the current phase with - * {@link SubscriptionState#advancePhase state.advancePhase()}, - * {@link SubscriptionState#advancePhaseBy(int) state.advancedPhaseBy(int)} and - * {@link SubscriptionState#phase(int) state.phase(int)}.
    • - *
    - *

    - * When you implement {@code AbstractOnSubscribe}, you may override {@link AbstractOnSubscribe#onSubscribe} to - * perform special actions (such as registering {@code Subscription}s with {@code Subscriber.add()}) and return - * additional state for each subscriber subscribing. You can access this custom state with the - * {@link SubscriptionState#state state.state()} method. If you need to do some cleanup, you can override the - * {@link #onTerminated} method. - *

    - * For convenience, a lambda-accepting static factory method, {@link #create}, is available. - * Another convenience is {@link #toObservable} which turns an {@code AbstractOnSubscribe} - * instance into an {@code Observable} fluently. - * - *

    Examples

    - * Note: these examples use the lambda-helper factories to avoid boilerplate. - * - *

    Implement: just

    - *
    
    - * AbstractOnSubscribe.create(s -> {
    - *   s.onNext(1);
    - *   s.onCompleted();
    - * }).toObservable().subscribe(System.out::println);
    - * 
    - - *

    Implement: from Iterable

    - *
    
    - * Iterable iterable = ...;
    - * AbstractOnSubscribe.create(s -> {
    - *   Iterator it = s.state();
    - *   if (it.hasNext()) {
    - *     s.onNext(it.next());
    - *   }
    - *   if (!it.hasNext()) {
    - *     s.onCompleted();
    - *   }
    - * }, u -> iterable.iterator()).subscribe(System.out::println);
    - * 
    - * - *

    Implement source that fails a number of times before succeeding

    - *
    
    - * AtomicInteger fails = new AtomicInteger();
    - * int numFails = 50;
    - * AbstractOnSubscribe.create(s -> {
    - *   long c = s.calls();
    - *   switch (s.phase()) {
    - *   case 0:
    - *     s.onNext("Beginning");
    - *     s.onError(new RuntimeException("Oh, failure.");
    - *     if (c == numFails.getAndIncrement()) {
    - *       s.advancePhase();
    - *     }
    - *     break;
    - *   case 1:
    - *     s.onNext("Beginning");
    - *     s.advancePhase();
    - *   case 2:
    - *     s.onNext("Finally working");
    - *     s.onCompleted();
    - *     s.advancePhase();
    - *   default:
    - *     throw new IllegalStateException("How did we get here?");
    - *   }
    - * }).subscribe(System.out::println);
    - * 
    - - *

    Implement: never

    - *
    
    - * AbstractOnSubscribe.create(s -> {
    - *   s.stop();
    - * }).toObservable()
    - * .timeout(1, TimeUnit.SECONDS)
    - * .subscribe(System.out::println, Throwable::printStacktrace, () -> System.out.println("Done"));
    - * 
    - * - * @param the value type - * @param the per-subscriber user-defined state type - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) - * @Experimental - */ -@Experimental -public abstract class AbstractOnSubscribe implements OnSubscribe { - /** - * Called when a Subscriber subscribes and lets the implementor create a per-subscriber custom state. - *

    - * Override this method to have custom state per-subscriber. The default implementation returns - * {@code null}. - * - * @param subscriber the subscriber who is subscribing - * @return the custom state - */ - protected S onSubscribe(Subscriber subscriber) { - return null; - } - - /** - * Called after the terminal emission or when the downstream unsubscribes. - *

    - * This is called only once and no {@code onNext} call will run concurrently with it. The default - * implementation does nothing. - * - * @param state the user-provided state - */ - protected void onTerminated(S state) { - - } - - /** - * Override this method to create an emission state-machine. - * - * @param state the per-subscriber subscription state - */ - protected abstract void next(SubscriptionState state); - - @Override - public final void call(final Subscriber subscriber) { - final S custom = onSubscribe(subscriber); - final SubscriptionState state = new SubscriptionState(this, subscriber, custom); - subscriber.add(new SubscriptionCompleter(state)); - subscriber.setProducer(new SubscriptionProducer(state)); - } - - /** - * Convenience method to create an Observable from this implemented instance. - * - * @return the created observable - */ - public final Observable toObservable() { - return Observable.create(this); - } - - /** Function that returns null. */ - private static final Func1 NULL_FUNC1 = new Func1() { - @Override - public Object call(Object t1) { - return null; - } - }; - - /** - * Creates an {@code AbstractOnSubscribe} instance which calls the provided {@code next} action. - *

    - * This is a convenience method to help create {@code AbstractOnSubscribe} instances with the help of - * lambdas. - * - * @param the value type - * @param the per-subscriber user-defined state type - * @param next the next action to call - * @return an {@code AbstractOnSubscribe} instance - */ - public static AbstractOnSubscribe create(Action1> next) { - @SuppressWarnings("unchecked") - Func1, ? extends S> nullFunc = - (Func1, ? extends S>)NULL_FUNC1; - return create(next, nullFunc, Actions.empty()); - } - - /** - * Creates an {@code AbstractOnSubscribe} instance which creates a custom state with the {@code onSubscribe} - * function and calls the provided {@code next} action. - *

    - * This is a convenience method to help create {@code AbstractOnSubscribe} instances with the help of - * lambdas. - * - * @param the value type - * @param the per-subscriber user-defined state type - * @param next the next action to call - * @param onSubscribe the function that returns a per-subscriber state to be used by {@code next} - * @return an {@code AbstractOnSubscribe} instance - */ - public static AbstractOnSubscribe create(Action1> next, - Func1, ? extends S> onSubscribe) { - return create(next, onSubscribe, Actions.empty()); - } - - /** - * Creates an {@code AbstractOnSubscribe} instance which creates a custom state with the {@code onSubscribe} - * function, calls the provided {@code next} action and calls the {@code onTerminated} action to release the - * state when its no longer needed. - *

    - * This is a convenience method to help create {@code AbstractOnSubscribe} instances with the help of - * lambdas. - * - * @param the value type - * @param the per-subscriber user-defined state type - * @param next the next action to call - * @param onSubscribe the function that returns a per-subscriber state to be used by {@code next} - * @param onTerminated the action to call to release the state created by the {@code onSubscribe} function - * @return an {@code AbstractOnSubscribe} instance - */ - public static AbstractOnSubscribe create(Action1> next, - Func1, ? extends S> onSubscribe, Action1 onTerminated) { - return new LambdaOnSubscribe(next, onSubscribe, onTerminated); - } - - /** - * An implementation that forwards the three main methods ({@code next}, {@code onSubscribe}, and - * {@code onTermianted}) to functional callbacks. - * - * @param the value type - * @param the per-subscriber user-defined state type - */ - private static final class LambdaOnSubscribe extends AbstractOnSubscribe { - final Action1> next; - final Func1, ? extends S> onSubscribe; - final Action1 onTerminated; - private LambdaOnSubscribe(Action1> next, - Func1, ? extends S> onSubscribe, Action1 onTerminated) { - this.next = next; - this.onSubscribe = onSubscribe; - this.onTerminated = onTerminated; - } - @Override - protected S onSubscribe(Subscriber subscriber) { - return onSubscribe.call(subscriber); - } - @Override - protected void onTerminated(S state) { - onTerminated.call(state); - } - @Override - protected void next(SubscriptionState state) { - next.call(state); - } - } - - /** - * Manages unsubscription of the state. - * - * @param the value type - * @param the per-subscriber user-defined state type - */ - private static final class SubscriptionCompleter extends AtomicBoolean implements Subscription { - private static final long serialVersionUID = 7993888274897325004L; - private final SubscriptionState state; - private SubscriptionCompleter(SubscriptionState state) { - this.state = state; - } - @Override - public boolean isUnsubscribed() { - return get(); - } - @Override - public void unsubscribe() { - if (compareAndSet(false, true)) { - state.free(); - } - } - - } - /** - * Contains the producer loop that reacts to downstream requests of work. - * - * @param the value type - * @param the per-subscriber user-defined state type - */ - private static final class SubscriptionProducer implements Producer { - final SubscriptionState state; - private SubscriptionProducer(SubscriptionState state) { - this.state = state; - } - @Override - public void request(long n) { - if (n > 0 && BackpressureUtils.getAndAddRequest(state.requestCount, n) == 0) { - if (n == Long.MAX_VALUE) { - // fast-path - for (; !state.subscriber.isUnsubscribed(); ) { - if (!doNext()) { - break; - } - } - } else - if (!state.subscriber.isUnsubscribed()) { - do { - if (!doNext()) { - break; - } - } while (state.requestCount.decrementAndGet() > 0 && !state.subscriber.isUnsubscribed()); - } - } - } - - /** - * Executes the user-overridden next() method and performs state bookkeeping and - * verification. - * - * @return true if the outer loop may continue - */ - protected boolean doNext() { - if (state.use()) { - try { - int p = state.phase(); - state.parent.next(state); - if (!state.verify()) { - throw new IllegalStateException("No event produced or stop called @ Phase: " + p + " -> " + state.phase() + ", Calls: " + state.calls()); - } - if (state.accept() || state.stopRequested()) { - state.terminate(); - return false; - } - state.calls++; - } catch (Throwable t) { - state.terminate(); - state.subscriber.onError(t); - return false; - } finally { - state.free(); - } - return true; - } - return false; - } - } - - /** - * Represents a per-subscription state for the {@code AbstractOnSubscribe} operation. It supports phasing - * and counts the number of times a value was requested by the downstream. - * - * @param the value type - * @param the per-subscriber user-defined state type - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) - * @Experimental - */ - public static final class SubscriptionState { - private final AbstractOnSubscribe parent; - private final Subscriber subscriber; - private final S state; - private final AtomicLong requestCount; - private final AtomicInteger inUse; - private int phase; - private long calls; - private T theValue; - private boolean hasOnNext; - private boolean hasCompleted; - private boolean stopRequested; - private Throwable theException; - private SubscriptionState(AbstractOnSubscribe parent, Subscriber subscriber, S state) { - this.parent = parent; - this.subscriber = subscriber; - this.state = state; - this.requestCount = new AtomicLong(); - this.inUse = new AtomicInteger(1); - } - - /** - * @return the per-subscriber specific user-defined state created via - * {@link AbstractOnSubscribe#onSubscribe} - */ - public S state() { - return state; - } - - /** - * @return the current phase value - */ - public int phase() { - return phase; - } - - /** - * Sets a new phase value. - * - * @param newPhase - */ - public void phase(int newPhase) { - phase = newPhase; - } - - /** - * Advance the current phase by 1. - */ - public void advancePhase() { - advancePhaseBy(1); - } - - /** - * Advance the current phase by the given amount (can be negative). - * - * @param amount the amount to advance the phase - */ - public void advancePhaseBy(int amount) { - phase += amount; - } - - /** - * @return the number of times {@link AbstractOnSubscribe#next} was called so far, starting at 0 for the - * very first call - */ - public long calls() { - return calls; - } - - /** - * Call this method to offer the next {@code onNext} value for the subscriber. - * - * @param value the value to {@code onNext} - * @throws IllegalStateException if there is a value already offered but not taken or a terminal state - * is reached - */ - public void onNext(T value) { - if (hasOnNext) { - throw new IllegalStateException("onNext not consumed yet!"); - } else - if (hasCompleted) { - throw new IllegalStateException("Already terminated", theException); - } - theValue = value; - hasOnNext = true; - } - - /** - * Call this method to send an {@code onError} to the subscriber and terminate all further activities. - * If there is a pending {@code onNext}, that value is emitted to the subscriber followed by this - * exception. - * - * @param e the exception to deliver to the client - * @throws IllegalStateException if the terminal state has been reached already - */ - public void onError(Throwable e) { - if (e == null) { - throw new NullPointerException("e != null required"); - } - if (hasCompleted) { - throw new IllegalStateException("Already terminated", theException); - } - theException = e; - hasCompleted = true; - } - - /** - * Call this method to send an {@code onCompleted} to the subscriber and terminate all further - * activities. If there is a pending {@code onNext}, that value is emitted to the subscriber followed by - * this exception. - * - * @throws IllegalStateException if the terminal state has been reached already - */ - public void onCompleted() { - if (hasCompleted) { - throw new IllegalStateException("Already terminated", theException); - } - hasCompleted = true; - } - - /** - * Signals that there won't be any further events. - */ - public void stop() { - stopRequested = true; - } - - /** - * Emits the {@code onNext} and/or the terminal value to the actual subscriber. - * - * @return {@code true} if the event was a terminal event - */ - protected boolean accept() { - if (hasOnNext) { - T value = theValue; - theValue = null; - hasOnNext = false; - - try { - subscriber.onNext(value); - } catch (Throwable t) { - hasCompleted = true; - Throwable e = theException; - theException = null; - if (e == null) { - subscriber.onError(t); - } else { - subscriber.onError(new CompositeException(Arrays.asList(t, e))); - } - return true; - } - } - if (hasCompleted) { - Throwable e = theException; - theException = null; - - if (e != null) { - subscriber.onError(e); - } else { - subscriber.onCompleted(); - } - return true; - } - return false; - } - - /** - * Verify if the {@code next()} generated an event or requested a stop. - * - * @return true if either event was generated or stop was requested - */ - protected boolean verify() { - return hasOnNext || hasCompleted || stopRequested; - } - - /** @return true if the {@code next()} requested a stop */ - protected boolean stopRequested() { - return stopRequested; - } - - /** - * Request the state to be used by {@code onNext} or returns {@code false} if the downstream has - * unsubscribed. - * - * @return {@code true} if the state can be used exclusively - * @throws IllegalStateEception - * @warn "throws" section incomplete - */ - protected boolean use() { - int i = inUse.get(); - if (i == 0) { - return false; - } else - if (i == 1 && inUse.compareAndSet(1, 2)) { - return true; - } - throw new IllegalStateException("This is not reentrant nor threadsafe!"); - } - - /** - * Release the state if there are no more interest in it and it is not in use. - */ - protected void free() { - int i = inUse.get(); - if (i > 0 && inUse.decrementAndGet() == 0) { - parent.onTerminated(state); - } - } - - /** - * Terminates the state immediately and calls {@link AbstractOnSubscribe#onTerminated} with the custom - * state. - */ - protected void terminate() { - for (;;) { - int i = inUse.get(); - if (i <= 0) { - return; - } - if (inUse.compareAndSet(i, 0)) { - parent.onTerminated(state); - break; - } - } - } - } -} diff --git a/src/main/java/rx/observables/ConnectableObservable.java b/src/main/java/rx/observables/ConnectableObservable.java index 868c2d3071..fe78ae55cd 100644 --- a/src/main/java/rx/observables/ConnectableObservable.java +++ b/src/main/java/rx/observables/ConnectableObservable.java @@ -16,7 +16,7 @@ package rx.observables; import rx.*; -import rx.annotations.Experimental; +import rx.annotations.Beta; import rx.functions.*; import rx.internal.operators.*; @@ -88,7 +88,7 @@ public Observable refCount() { * when the first Subscriber subscribes * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ - @Experimental + @Beta public Observable autoConnect() { return autoConnect(1); } @@ -103,7 +103,7 @@ public Observable autoConnect() { * when the specified number of Subscribers subscribe to it * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ - @Experimental + @Beta public Observable autoConnect(int numberOfSubscribers) { return autoConnect(numberOfSubscribers, Actions.empty()); } @@ -123,7 +123,7 @@ public Observable autoConnect(int numberOfSubscribers) { * specified callback with the Subscription associated with the established connection * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ - @Experimental + @Beta public Observable autoConnect(int numberOfSubscribers, Action1 connection) { if (numberOfSubscribers <= 0) { this.connect(connection); diff --git a/src/main/java/rx/observers/Subscribers.java b/src/main/java/rx/observers/Subscribers.java index 4e81c1af8d..c1d2e4d014 100644 --- a/src/main/java/rx/observers/Subscribers.java +++ b/src/main/java/rx/observers/Subscribers.java @@ -15,12 +15,9 @@ */ package rx.observers; -import rx.Observer; -import rx.Subscriber; -import rx.annotations.Experimental; +import rx.*; import rx.exceptions.OnErrorNotImplementedException; -import rx.functions.Action0; -import rx.functions.Action1; +import rx.functions.*; /** * Helper methods and utilities for creating and working with {@link Subscriber} objects. @@ -213,9 +210,8 @@ public final void onNext(T args) { * subscriber, has backpressure controlled by * subscriber and uses subscriber to * manage unsubscription. - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public static Subscriber wrap(final Subscriber subscriber) { return new Subscriber(subscriber) { diff --git a/src/main/java/rx/observers/TestSubscriber.java b/src/main/java/rx/observers/TestSubscriber.java index 2d46a25179..798ada4cc3 100644 --- a/src/main/java/rx/observers/TestSubscriber.java +++ b/src/main/java/rx/observers/TestSubscriber.java @@ -20,7 +20,6 @@ import rx.*; import rx.Observer; -import rx.annotations.Experimental; import rx.exceptions.CompositeException; /** @@ -58,10 +57,9 @@ public void onNext(Object t) { * Constructs a TestSubscriber with the initial request to be requested from upstream. * * @param initialRequest the initial request value, negative value will revert to the default unbounded behavior - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ @SuppressWarnings("unchecked") - @Experimental public TestSubscriber(long initialRequest) { this((Observer)INERT, initialRequest); } @@ -72,9 +70,9 @@ public TestSubscriber(long initialRequest) { * * @param initialRequest the initial request value, negative value will revert to the default unbounded behavior * @param delegate the Observer instance to wrap - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @throws NullPointerException if delegate is null + * @since 1.1.0 */ - @Experimental public TestSubscriber(Observer delegate, long initialRequest) { if (delegate == null) { throw new NullPointerException(); @@ -83,39 +81,87 @@ public TestSubscriber(Observer delegate, long initialRequest) { this.initialRequest = initialRequest; } + /** + * Constructs a TestSubscriber which requests Long.MAX_VALUE and delegates events to + * the given Subscriber. + * @param delegate the subscriber to delegate to. + * @throws NullPointerException if delegate is null + * @since 1.1.0 + */ public TestSubscriber(Subscriber delegate) { this(delegate, -1); } + /** + * Constructs a TestSubscriber which requests Long.MAX_VALUE and delegates events to + * the given Observer. + * @param delegate the observer to delegate to. + * @throws NullPointerException if delegate is null + * @since 1.1.0 + */ public TestSubscriber(Observer delegate) { this(delegate, -1); } + /** + * Constructs a TestSubscriber with an initial request of Long.MAX_VALUE and no delegation. + */ public TestSubscriber() { this(-1); } - - @Experimental + + /** + * Factory method to construct a TestSubscriber with an initial request of Long.MAX_VALUE and no delegation. + * @return the created TestSubscriber instance + * @since 1.1.0 + */ public static TestSubscriber create() { return new TestSubscriber(); } - @Experimental + /** + * Factory method to construct a TestSubscriber with the given initial request amount and no delegation. + * @param initialRequest the initial request amount, negative values revert to the default unbounded mode + * @return the created TestSubscriber instance + * @since 1.1.0 + */ public static TestSubscriber create(long initialRequest) { return new TestSubscriber(initialRequest); } - @Experimental + /** + * Factory method to construct a TestSubscriber which delegates events to the given Observer and + * issues the given initial request amount. + * @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 + * @throws NullPointerException if delegate is null + * @since 1.1.0 + */ public static TestSubscriber create(Observer delegate, long initialRequest) { return new TestSubscriber(delegate, initialRequest); } - @Experimental + /** + * Factory method to construct a TestSubscriber which delegates events to the given Observer and + * an issues an initial request of Long.MAX_VALUE. + * @param delegate the observer to delegate events to + * @return the created TestSubscriber instance + * @throws NullPointerException if delegate is null + * @since 1.1.0 + */ public static TestSubscriber create(Subscriber delegate) { return new TestSubscriber(delegate); } - @Experimental + /** + * Factory method to construct a TestSubscriber which delegates events to the given Subscriber and + * an issues an initial request of Long.MAX_VALUE. + * @param delegate the subscriber to delegate events to + * @return the created TestSubscriber instance + * @throws NullPointerException if delegate is null + * @since 1.1.0 + */ public static TestSubscriber create(Observer delegate) { return new TestSubscriber(delegate); } @@ -343,9 +389,8 @@ public Thread getLastSeenThread() { * Asserts that there is exactly one completion event. * * @throws AssertionError if there were zero, or more than one, onCompleted events - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertCompleted() { int s = testObserver.getOnCompletedEvents().size(); if (s == 0) { @@ -360,9 +405,8 @@ public void assertCompleted() { * Asserts that there is no completion event. * * @throws AssertionError if there were one or more than one onCompleted events - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertNotCompleted() { int s = testObserver.getOnCompletedEvents().size(); if (s == 1) { @@ -379,9 +423,8 @@ public void assertNotCompleted() { * @param clazz the class to check the error against. * @throws AssertionError if there were zero, or more than one, onError events, or if the single onError * event did not carry an error of a subclass of the given class - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertError(Class clazz) { List err = testObserver.getOnErrorEvents(); if (err.size() == 0) { @@ -405,9 +448,8 @@ public void assertError(Class clazz) { * @param throwable the throwable to check * @throws AssertionError if there were zero, or more than one, onError events, or if the single onError * event did not carry an error that matches the specified throwable - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertError(Throwable throwable) { List err = testObserver.getOnErrorEvents(); if (err.size() == 0) { @@ -429,9 +471,8 @@ public void assertError(Throwable throwable) { * Asserts that there are no onError and onCompleted events. * * @throws AssertionError if there was either an onError or onCompleted event - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertNoTerminalEvent() { List err = testObserver.getOnErrorEvents(); int s = testObserver.getOnCompletedEvents().size(); @@ -455,9 +496,8 @@ public void assertNoTerminalEvent() { * Asserts that there are no onNext events received. * * @throws AssertionError if there were any onNext events - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertNoValues() { int s = testObserver.getOnNextEvents().size(); if (s > 0) { @@ -470,9 +510,8 @@ public void assertNoValues() { * * @param count the expected number of onNext events * @throws AssertionError if there were more or fewer onNext events than specified by {@code count} - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertValueCount(int count) { int s = testObserver.getOnNextEvents().size(); if (s != count) { @@ -485,9 +524,8 @@ public void assertValueCount(int count) { * * @param values the items to check * @throws AssertionError if the items emitted do not exactly match those specified by {@code values} - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertValues(T... values) { assertReceivedOnNext(Arrays.asList(values)); } @@ -497,9 +535,8 @@ public void assertValues(T... values) { * * @param value the item to check * @throws AssertionError if the Observable does not emit only the single item specified by {@code value} - * @since (if this graduates from "Experimental" replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public void assertValue(T value) { assertReceivedOnNext(Collections.singletonList(value)); } diff --git a/src/main/java/rx/plugins/RxJavaErrorHandler.java b/src/main/java/rx/plugins/RxJavaErrorHandler.java index 85a21d447a..a6e56475ed 100644 --- a/src/main/java/rx/plugins/RxJavaErrorHandler.java +++ b/src/main/java/rx/plugins/RxJavaErrorHandler.java @@ -15,9 +15,8 @@ */ package rx.plugins; -import rx.Observable; -import rx.Subscriber; -import rx.annotations.Experimental; +import rx.*; +import rx.annotations.Beta; import rx.exceptions.Exceptions; /** @@ -67,7 +66,7 @@ public void handleError(Throwable e) { * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the * release number) */ - @Experimental + @Beta public final String handleOnNextValueRendering(Object item) { try { @@ -98,7 +97,7 @@ public final String handleOnNextValueRendering(Object item) { * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the * release number) */ - @Experimental + @Beta protected String render (Object item) throws InterruptedException { //do nothing by default return null; diff --git a/src/main/java/rx/subjects/AsyncSubject.java b/src/main/java/rx/subjects/AsyncSubject.java index b124b8966c..57539fa8eb 100644 --- a/src/main/java/rx/subjects/AsyncSubject.java +++ b/src/main/java/rx/subjects/AsyncSubject.java @@ -15,11 +15,10 @@ */ package rx.subjects; -import java.lang.reflect.Array; import java.util.*; import rx.Observer; -import rx.annotations.Experimental; +import rx.annotations.Beta; import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.internal.operators.NotificationLite; @@ -141,8 +140,7 @@ public boolean hasObservers() { * retrieved by {@code getValue()} may get outdated. * @return true if and only if the subject has some value but not an error */ - @Experimental - @Override + @Beta public boolean hasValue() { Object v = lastValue; Object o = state.getLatest(); @@ -152,8 +150,7 @@ public boolean hasValue() { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ - @Experimental - @Override + @Beta public boolean hasThrowable() { Object o = state.getLatest(); return nl.isError(o); @@ -162,8 +159,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted()} */ - @Experimental - @Override + @Beta public boolean hasCompleted() { Object o = state.getLatest(); return o != null && !nl.isError(o); @@ -177,8 +173,7 @@ public boolean hasCompleted() { * @return the current value or {@code null} if the Subject doesn't have a value, * has terminated with an exception or has an actual {@code null} as a value. */ - @Experimental - @Override + @Beta public T getValue() { Object v = lastValue; Object o = state.getLatest(); @@ -192,8 +187,7 @@ public T getValue() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ - @Experimental - @Override + @Beta public Throwable getThrowable() { Object o = state.getLatest(); if (nl.isError(o)) { @@ -201,26 +195,4 @@ public Throwable getThrowable() { } return null; } - @Override - @Experimental - @Deprecated - @SuppressWarnings("unchecked") - public T[] getValues(T[] a) { - Object v = lastValue; - Object o = state.getLatest(); - if (!nl.isError(o) && nl.isNext(v)) { - T val = nl.getValue(v); - if (a.length == 0) { - a = (T[])Array.newInstance(a.getClass().getComponentType(), 1); - } - a[0] = val; - if (a.length > 1) { - a[1] = null; - } - } else - if (a.length > 0) { - a[0] = null; - } - return a; - } } diff --git a/src/main/java/rx/subjects/BehaviorSubject.java b/src/main/java/rx/subjects/BehaviorSubject.java index d912e81411..ad8bd448f6 100644 --- a/src/main/java/rx/subjects/BehaviorSubject.java +++ b/src/main/java/rx/subjects/BehaviorSubject.java @@ -20,7 +20,7 @@ import java.util.*; import rx.Observer; -import rx.annotations.Experimental; +import rx.annotations.Beta; import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.internal.operators.NotificationLite; @@ -177,8 +177,7 @@ public boolean hasObservers() { * retrieved by {@code getValue()} may get outdated. * @return true if and only if the subject has some value and hasn't terminated yet. */ - @Experimental - @Override + @Beta public boolean hasValue() { Object o = state.getLatest(); return nl.isNext(o); @@ -187,8 +186,7 @@ public boolean hasValue() { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ - @Experimental - @Override + @Beta public boolean hasThrowable() { Object o = state.getLatest(); return nl.isError(o); @@ -197,8 +195,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted()} */ - @Experimental - @Override + @Beta public boolean hasCompleted() { Object o = state.getLatest(); return nl.isCompleted(o); @@ -212,8 +209,7 @@ public boolean hasCompleted() { * @return the current value or {@code null} if the Subject doesn't have a value, * has terminated or has an actual {@code null} as a valid value. */ - @Experimental - @Override + @Beta public T getValue() { Object o = state.getLatest(); if (nl.isNext(o)) { @@ -226,8 +222,7 @@ public T getValue() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ - @Experimental - @Override + @Beta public Throwable getThrowable() { Object o = state.getLatest(); if (nl.isError(o)) { @@ -235,8 +230,13 @@ public Throwable getThrowable() { } return null; } - @Override - @Experimental + /** + * Returns a snapshot of the currently buffered non-terminal events into + * the provided {@code a} array or creates a new array if it has not enough capacity. + * @param a the array to fill in + * @return the array {@code a} if it had enough capacity or a new array containing the available values + */ + @Beta @SuppressWarnings("unchecked") public T[] getValues(T[] a) { Object o = state.getLatest(); @@ -254,4 +254,24 @@ public T[] getValues(T[] a) { } return a; } + + /** An empty array to trigger getValues() to return a new array. */ + private static final Object[] EMPTY_ARRAY = new Object[0]; + + /** + * Returns a snapshot of the currently buffered non-terminal events. + *

    The operation is threadsafe. + * + * @return a snapshot of the currently buffered non-terminal events. + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + */ + @SuppressWarnings("unchecked") + @Beta + public Object[] getValues() { + T[] r = getValues((T[])EMPTY_ARRAY); + if (r == EMPTY_ARRAY) { + return new Object[0]; // don't leak the default empty array. + } + return r; + } } diff --git a/src/main/java/rx/subjects/PublishSubject.java b/src/main/java/rx/subjects/PublishSubject.java index f9dd1f0e4f..42a4a18c7c 100644 --- a/src/main/java/rx/subjects/PublishSubject.java +++ b/src/main/java/rx/subjects/PublishSubject.java @@ -18,7 +18,7 @@ import java.util.*; import rx.Observer; -import rx.annotations.Experimental; +import rx.annotations.Beta; import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.internal.operators.NotificationLite; @@ -124,8 +124,7 @@ public boolean hasObservers() { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ - @Experimental - @Override + @Beta public boolean hasThrowable() { Object o = state.getLatest(); return nl.isError(o); @@ -134,8 +133,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted} */ - @Experimental - @Override + @Beta public boolean hasCompleted() { Object o = state.getLatest(); return o != null && !nl.isError(o); @@ -145,8 +143,7 @@ public boolean hasCompleted() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ - @Experimental - @Override + @Beta public Throwable getThrowable() { Object o = state.getLatest(); if (nl.isError(o)) { @@ -154,49 +151,4 @@ public Throwable getThrowable() { } return null; } - - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public boolean hasValue() { - return false; - } - - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public T getValue() { - return null; - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public Object[] getValues() { - return new Object[0]; - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public T[] getValues(T[] a) { - if (a.length > 0) { - a[0] = null; - } - return a; - } } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index 7ed517375e..ca166b6177 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -23,7 +23,7 @@ import rx.Observer; import rx.Scheduler; -import rx.annotations.Experimental; +import rx.annotations.Beta; import rx.exceptions.Exceptions; import rx.functions.Action1; import rx.functions.Func1; @@ -1097,8 +1097,7 @@ public void evictFinal(NodeList list) { * Check if the Subject has terminated with an exception. * @return true if the subject has received a throwable through {@code onError}. */ - @Experimental - @Override + @Beta public boolean hasThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.getLatest(); @@ -1108,8 +1107,7 @@ public boolean hasThrowable() { * Check if the Subject has terminated normally. * @return true if the subject completed normally via {@code onCompleted} */ - @Experimental - @Override + @Beta public boolean hasCompleted() { NotificationLite nl = ssm.nl; Object o = ssm.getLatest(); @@ -1120,8 +1118,7 @@ public boolean hasCompleted() { * @return the Throwable that terminated the Subject or {@code null} if the * subject hasn't terminated yet or it terminated normally. */ - @Experimental - @Override + @Beta public Throwable getThrowable() { NotificationLite nl = ssm.nl; Object o = ssm.getLatest(); @@ -1134,19 +1131,18 @@ public Throwable getThrowable() { * Returns the current number of items (non-terminal events) available for replay. * @return the number of items available */ - @Experimental + @Beta public int size() { return state.size(); } /** * @return true if the Subject holds at least one non-terminal event available for replay */ - @Experimental + @Beta public boolean hasAnyValue() { return !state.isEmpty(); } - @Experimental - @Override + @Beta public boolean hasValue() { return hasAnyValue(); } @@ -1156,14 +1152,32 @@ public boolean hasValue() { * @param a the array to fill in * @return the array {@code a} if it had enough capacity or a new array containing the available values */ - @Experimental - @Override + @Beta public T[] getValues(T[] a) { return state.toArray(a); } - @Override - @Experimental + /** An empty array to trigger getValues() to return a new array. */ + private static final Object[] EMPTY_ARRAY = new Object[0]; + + /** + * Returns a snapshot of the currently buffered non-terminal events. + *

    The operation is threadsafe. + * + * @return a snapshot of the currently buffered non-terminal events. + * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) + */ + @SuppressWarnings("unchecked") + @Beta + public Object[] getValues() { + T[] r = getValues((T[])EMPTY_ARRAY); + if (r == EMPTY_ARRAY) { + return new Object[0]; // don't leak the default empty array. + } + return r; + } + + @Beta public T getValue() { return state.latest(); } diff --git a/src/main/java/rx/subjects/SerializedSubject.java b/src/main/java/rx/subjects/SerializedSubject.java index 6dd5a46592..33b532c7b4 100644 --- a/src/main/java/rx/subjects/SerializedSubject.java +++ b/src/main/java/rx/subjects/SerializedSubject.java @@ -16,7 +16,6 @@ package rx.subjects; import rx.Subscriber; -import rx.annotations.Experimental; import rx.observers.SerializedObserver; /** @@ -69,75 +68,4 @@ public void onNext(T t) { public boolean hasObservers() { return actual.hasObservers(); } - - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public boolean hasCompleted() { - return actual.hasCompleted(); - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public boolean hasThrowable() { - return actual.hasThrowable(); - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public boolean hasValue() { - return actual.hasValue(); - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public Throwable getThrowable() { - return actual.getThrowable(); - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public T getValue() { - return actual.getValue(); - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public Object[] getValues() { - return actual.getValues(); - } - /** - * {@inheritDoc} - * @deprecated this method is scheduled to be removed in the next release - */ - @Override - @Experimental - @Deprecated - public T[] getValues(T[] a) { - return actual.getValues(a); - } } diff --git a/src/main/java/rx/subjects/Subject.java b/src/main/java/rx/subjects/Subject.java index b220cc1b51..94a289139a 100644 --- a/src/main/java/rx/subjects/Subject.java +++ b/src/main/java/rx/subjects/Subject.java @@ -15,10 +15,7 @@ */ package rx.subjects; -import rx.Observable; -import rx.Observer; -import rx.Subscriber; -import rx.annotations.Experimental; +import rx.*; /** * Represents an object that is both an Observable and an Observer. @@ -58,117 +55,4 @@ public final SerializedSubject toSerialized() { } return new SerializedSubject(this); } - /** - * Check if the Subject has terminated with an exception. - *

    The operation is threadsafe. - * - * @return {@code true} if the subject has received a throwable through {@code onError}. - * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) - * @deprecated this method will be moved to each Subject class individually in the next release - */ - @Experimental - @Deprecated - public boolean hasThrowable() { - throw new UnsupportedOperationException(); - } - /** - * Check if the Subject has terminated normally. - *

    The operation is threadsafe. - * - * @return {@code true} if the subject completed normally via {@code onCompleted} - * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) - * @deprecated this method will be moved to each Subject class individually in the next release - */ - @Experimental - @Deprecated - public boolean hasCompleted() { - throw new UnsupportedOperationException(); - } - /** - * Returns the Throwable that terminated the Subject. - *

    The operation is threadsafe. - * - * @return the Throwable that terminated the Subject or {@code null} if the subject hasn't terminated yet or - * if it terminated normally. - * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) - * @deprecated this method will be moved to each Subject class individually in the next release - */ - @Experimental - @Deprecated - public Throwable getThrowable() { - throw new UnsupportedOperationException(); - } - /** - * Check if the Subject has any value. - *

    Use the {@link #getValue()} method to retrieve such a value. - *

    Note that unless {@link #hasCompleted()} or {@link #hasThrowable()} returns true, the value - * retrieved by {@code getValue()} may get outdated. - *

    The operation is threadsafe. - * - * @return {@code true} if and only if the subject has some value but not an error - * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) - * @deprecated this method will be moved to each Subject class individually in the next release - */ - @Experimental - @Deprecated - public boolean hasValue() { - throw new UnsupportedOperationException(); - } - /** - * Returns the current or latest value of the Subject if there is such a value and - * the subject hasn't terminated with an exception. - *

    The method can return {@code null} for various reasons. Use {@link #hasValue()}, {@link #hasThrowable()} - * and {@link #hasCompleted()} to determine if such {@code null} is a valid value, there was an - * exception or the Subject terminated without receiving any value. - *

    The operation is threadsafe. - * - * @return the current value or {@code null} if the Subject doesn't have a value, has terminated with an - * exception or has an actual {@code null} as a value. - * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) - * @deprecated this method will be moved to each Subject class individually in the next release - */ - @Experimental - @Deprecated - public T getValue() { - throw new UnsupportedOperationException(); - } - /** An empty array to trigger getValues() to return a new array. */ - private static final Object[] EMPTY_ARRAY = new Object[0]; - /** - * Returns a snapshot of the currently buffered non-terminal events. - *

    The operation is threadsafe. - * - * @return a snapshot of the currently buffered non-terminal events. - * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) - * @deprecated this method will be moved to each Subject class individually in the next release - */ - @SuppressWarnings("unchecked") - @Experimental - @Deprecated - public Object[] getValues() { - T[] r = getValues((T[])EMPTY_ARRAY); - if (r == EMPTY_ARRAY) { - return new Object[0]; // don't leak the default empty array. - } - return r; - } - /** - * Returns a snapshot of the currently buffered non-terminal events into - * the provided {@code a} array or creates a new array if it has not enough capacity. - *

    If the subject's values fit in the specified array with room to spare - * (i.e., the array has more elements than the list), the element in - * the array immediately following the end of the subject's values is set to - * {@code null}. - *

    The operation is threadsafe. - * - * @param a the array to fill in - * @return the array {@code a} if it had enough capacity or a new array containing the available values - * @since (If this graduates from being an Experimental class method, replace this parenthetical with the release number) - * @deprecated this method will be moved to each Subject class individually in the next release - */ - @Experimental - @Deprecated - public T[] getValues(T[] a) { - throw new UnsupportedOperationException(); - } } diff --git a/src/main/java/rx/subscriptions/Subscriptions.java b/src/main/java/rx/subscriptions/Subscriptions.java index bbc075a3a9..a86f5ef090 100644 --- a/src/main/java/rx/subscriptions/Subscriptions.java +++ b/src/main/java/rx/subscriptions/Subscriptions.java @@ -18,7 +18,6 @@ import java.util.concurrent.Future; import rx.Subscription; -import rx.annotations.Experimental; import rx.functions.Action0; /** @@ -57,9 +56,8 @@ public static Subscription empty() { * * * @return a {@link Subscription} to which {@code unsubscribe} does nothing, as it is already unsubscribed - * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) + * @since 1.1.0 */ - @Experimental public static Subscription unsubscribed() { return UNSUBSCRIBED; } diff --git a/src/perf/java/rx/observables/SyncOnSubscribePerf.java b/src/perf/java/rx/observables/SyncOnSubscribePerf.java index 8417bf3a8e..91882df8d0 100644 --- a/src/perf/java/rx/observables/SyncOnSubscribePerf.java +++ b/src/perf/java/rx/observables/SyncOnSubscribePerf.java @@ -80,36 +80,12 @@ public void benchFromIterable(final SingleInput input) { new OnSubscribeFromIterable(input.iterable).call(input.newSubscriber()); } -// @Benchmark -// @Group("single") - public void benchAbstractOnSubscribe(final SingleInput input) { - final Iterator iterator = input.iterable.iterator(); - createAbstractOnSubscribe(iterator).call(input.newSubscriber()); - } - - private AbstractOnSubscribe createAbstractOnSubscribe(final Iterator iterator) { - return new AbstractOnSubscribe() { - @Override - protected void next(rx.observables.AbstractOnSubscribe.SubscriptionState state) { - if (iterator.hasNext()) - state.onNext(iterator.next()); - else - state.onCompleted(); - }}; - } - @Benchmark // @Group("multi") public void benchSyncOnSubscribe2(final MultiInput input) { createSyncOnSubscribe(input.iterable.iterator()).call(input.newSubscriber()); } -// @Benchmark -// @Group("multi") - public void benchAbstractOnSubscribe2(final MultiInput input) { - createAbstractOnSubscribe(input.iterable.iterator()).call(input.newSubscriber()); - } - @Benchmark // @Group("multi") public void benchFromIterable2(final MultiInput input) { diff --git a/src/test/java/rx/internal/operators/OnBackpressureBlockTest.java b/src/test/java/rx/internal/operators/OnBackpressureBlockTest.java deleted file mode 100644 index 47d3cebd71..0000000000 --- a/src/test/java/rx/internal/operators/OnBackpressureBlockTest.java +++ /dev/null @@ -1,347 +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 static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Test; - -import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Observer; -import rx.Subscriber; -import rx.exceptions.MissingBackpressureException; -import rx.exceptions.TestException; -import rx.internal.util.RxRingBuffer; -import rx.observers.TestObserver; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.subjects.PublishSubject; - -/** - * Test the onBackpressureBlock() behavior. - */ -public class OnBackpressureBlockTest { - static final int WAIT = 200; - - @Test(timeout = 1000) - public void testSimpleBelowCapacity() { - Observable source = Observable.just(1).onBackpressureBlock(10); - - TestObserver o = new TestObserver(); - source.subscribe(o); - - o.assertReceivedOnNext(Arrays.asList(1)); - o.assertTerminalEvent(); - assertTrue(o.getOnErrorEvents().isEmpty()); - } - @Test(timeout = 10000) - public void testSimpleAboveCapacity() throws InterruptedException { - Observable source = Observable.range(1, 11).subscribeOn(Schedulers.newThread()) - .onBackpressureBlock(10); - - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - }; - source.subscribe(o); - o.requestMore(10); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - - o.requestMore(10); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)); - - o.assertTerminalEvent(); - assertTrue(o.getOnErrorEvents().isEmpty()); - } - - @Test(timeout = 3000) - public void testNoMissingBackpressureException() { - final int NUM_VALUES = RxRingBuffer.SIZE * 3; - Observable source = Observable.create(new OnSubscribe() { - @Override - public void call(Subscriber t1) { - for (int i = 0; i < NUM_VALUES; i++) { - t1.onNext(i); - } - t1.onCompleted(); - } - }).subscribeOn(Schedulers.newThread()); - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - TestSubscriber s = new TestSubscriber(o); - - source.onBackpressureBlock(RxRingBuffer.SIZE).observeOn(Schedulers.newThread()).subscribe(s); - - s.awaitTerminalEvent(); - - verify(o, never()).onError(any(MissingBackpressureException.class)); - - s.assertNoErrors(); - verify(o, times(NUM_VALUES)).onNext(any(Integer.class)); - verify(o).onCompleted(); - } - @Test(timeout = 10000) - public void testBlockedProducerCanBeUnsubscribed() throws InterruptedException { - Observable source = Observable.range(1, 11).subscribeOn(Schedulers.newThread()) - .onBackpressureBlock(5); - - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - }; - source.subscribe(o); - - o.requestMore(5); - - Thread.sleep(WAIT); - - o.unsubscribe(); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5)); - o.assertNoErrors(); - assertTrue(o.getOnCompletedEvents().isEmpty()); - } - @Test(timeout = 10000) - public void testExceptionIsDelivered() throws InterruptedException { - Observable source = Observable.range(1, 10) - .concatWith(Observable.error(new TestException("Forced failure"))) - .subscribeOn(Schedulers.newThread()) - .onBackpressureBlock(5); - - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - }; - source.subscribe(o); - - o.requestMore(7); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); - o.assertNoErrors(); - assertTrue(o.getOnCompletedEvents().isEmpty()); - - o.requestMore(3); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - o.assertTerminalEvent(); - assertEquals(1, o.getOnErrorEvents().size()); - assertTrue(o.getOnErrorEvents().get(0) instanceof TestException); - - o.requestMore(10); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - o.assertTerminalEvent(); - assertEquals(1, o.getOnErrorEvents().size()); - assertTrue(o.getOnErrorEvents().get(0) instanceof TestException); - } - @Test(timeout = 10000) - public void testExceptionIsDeliveredAfterValues() throws InterruptedException { - Observable source = Observable.range(1, 10) - .concatWith(Observable.error(new TestException("Forced failure"))) - .subscribeOn(Schedulers.newThread()) - .onBackpressureBlock(5); - - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - }; - source.subscribe(o); - - o.requestMore(7); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); - o.assertNoErrors(); - assertTrue(o.getOnCompletedEvents().isEmpty()); - - o.requestMore(7); - - Thread.sleep(WAIT); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - assertEquals(1, o.getOnErrorEvents().size()); - assertTrue(o.getOnErrorEvents().get(0) instanceof TestException); - assertTrue(o.getOnCompletedEvents().isEmpty()); - } - @Test(timeout = 10000) - public void testTakeWorksWithSubscriberRequesting() { - Observable source = Observable.range(1, 10) - .concatWith(Observable.error(new TestException("Forced failure"))) - .subscribeOn(Schedulers.newThread()) - .onBackpressureBlock(5).take(7); - - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - }; - source.subscribe(o); - - o.requestMore(7); - - o.awaitTerminalEvent(); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); - o.assertNoErrors(); - o.assertTerminalEvent(); - } - @Test(timeout = 10000) - public void testTakeWorksSubscriberRequestUnlimited() { - Observable source = Observable.range(1, 10) - .concatWith(Observable.error(new TestException("Forced failure"))) - .subscribeOn(Schedulers.newThread()) - .onBackpressureBlock(5).take(7); - - TestSubscriber o = new TestSubscriber(); - source.subscribe(o); - - o.awaitTerminalEvent(); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); - o.assertNoErrors(); - o.assertTerminalEvent(); - } - @Test(timeout = 10000) - public void testTakeWorksSubscriberRequestUnlimitedBufferedException() { - Observable source = Observable.range(1, 10) - .concatWith(Observable.error(new TestException("Forced failure"))) - .subscribeOn(Schedulers.newThread()) - .onBackpressureBlock(11).take(7); - - TestSubscriber o = new TestSubscriber(); - source.subscribe(o); - - o.awaitTerminalEvent(); - - o.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); - o.assertNoErrors(); - o.assertTerminalEvent(); - } - @Test(timeout = 10000) - public void testOnCompletedDoesntWaitIfNoEvents() { - - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - }; - Observable.empty().onBackpressureBlock(2).subscribe(o); - - o.assertNoErrors(); - o.assertTerminalEvent(); - o.assertReceivedOnNext(Collections.emptyList()); - } - @Test(timeout = 10000) - public void testOnCompletedDoesWaitIfEvents() { - - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - }; - Observable.just(1).onBackpressureBlock(2).subscribe(o); - - o.assertReceivedOnNext(Collections.emptyList()); - assertTrue(o.getOnErrorEvents().isEmpty()); - assertTrue(o.getOnCompletedEvents().isEmpty()); - } - @Test(timeout = 10000) - public void testOnCompletedDoesntWaitIfNoEvents2() { - final PublishSubject ps = PublishSubject.create(); - TestSubscriber o = new TestSubscriber() { - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - @Override - public void onNext(Integer t) { - super.onNext(t); - ps.onCompleted(); // as if an async completion arrived while in the loop - } - }; - ps.onBackpressureBlock(2).unsafeSubscribe(o); - ps.onNext(1); - o.requestMore(1); - - o.assertNoErrors(); - o.assertTerminalEvent(); - o.assertReceivedOnNext(Arrays.asList(1)); - } - @Test(timeout = 10000) - public void testOnCompletedDoesntWaitIfNoEvents3() { - final PublishSubject ps = PublishSubject.create(); - TestSubscriber o = new TestSubscriber() { - boolean once = true; - @Override - public void onStart() { - request(0); // make sure it doesn't start in unlimited mode - } - @Override - public void onNext(Integer t) { - super.onNext(t); - if (once) { - once = false; - ps.onNext(2); - ps.onCompleted(); // as if an async completion arrived while in the loop - requestMore(1); - } - } - }; - ps.onBackpressureBlock(3).unsafeSubscribe(o); - ps.onNext(1); - o.requestMore(1); - - o.assertNoErrors(); - o.assertTerminalEvent(); - o.assertReceivedOnNext(Arrays.asList(1, 2)); - } -} diff --git a/src/test/java/rx/internal/operators/OperatorScanTest.java b/src/test/java/rx/internal/operators/OperatorScanTest.java index 96c1b1dbe1..737bf1ce27 100644 --- a/src/test/java/rx/internal/operators/OperatorScanTest.java +++ b/src/test/java/rx/internal/operators/OperatorScanTest.java @@ -27,9 +27,9 @@ import rx.*; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.Observer; import rx.functions.*; -import rx.observables.AbstractOnSubscribe; import rx.observers.TestSubscriber; import rx.subjects.PublishSubject; @@ -371,12 +371,17 @@ public Integer call(Integer t1, Integer t2) { @Test public void testInitialValueEmittedWithProducer() { - Observable source = new AbstractOnSubscribe() { + Observable source = Observable.create(new OnSubscribe() { @Override - protected void next(rx.observables.AbstractOnSubscribe.SubscriptionState state) { - state.stop(); + public void call(Subscriber t) { + t.setProducer(new Producer() { + @Override + public void request(long n) { + // deliberately no op + } + }); } - }.toObservable(); + }); TestSubscriber ts = TestSubscriber.create(); diff --git a/src/test/java/rx/observables/AbstractOnSubscribeTest.java b/src/test/java/rx/observables/AbstractOnSubscribeTest.java deleted file mode 100644 index 95e3eac011..0000000000 --- a/src/test/java/rx/observables/AbstractOnSubscribeTest.java +++ /dev/null @@ -1,540 +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.observables; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.*; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; -import org.mockito.InOrder; - -import rx.*; -import rx.Observable; -import rx.Observer; -import rx.exceptions.TestException; -import rx.functions.*; -import rx.observables.AbstractOnSubscribe.SubscriptionState; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; - -/** - * Test if AbstractOnSubscribe adheres to the usual unsubscription and backpressure contracts. - */ -public class AbstractOnSubscribeTest { - @Test - public void testJust() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onNext(1); - state.onCompleted(); - } - }; - - TestSubscriber ts = new TestSubscriber(); - - aos.toObservable().subscribe(ts); - - ts.assertNoErrors(); - ts.assertTerminalEvent(); - ts.assertReceivedOnNext(Arrays.asList(1)); - } - @Test - public void testJustMisbehaving() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onNext(1); - state.onNext(2); - state.onCompleted(); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - aos.toObservable().subscribe(o); - - verify(o, never()).onNext(any(Integer.class)); - verify(o, never()).onCompleted(); - verify(o).onError(any(IllegalStateException.class)); - } - @Test - public void testJustMisbehavingOnCompleted() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onNext(1); - state.onCompleted(); - state.onCompleted(); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - aos.toObservable().subscribe(o); - - verify(o, never()).onNext(any(Integer.class)); - verify(o, never()).onCompleted(); - verify(o).onError(any(IllegalStateException.class)); - } - @Test - public void testJustMisbehavingOnError() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onNext(1); - state.onError(new TestException("Forced failure 1")); - state.onError(new TestException("Forced failure 2")); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - aos.toObservable().subscribe(o); - - verify(o, never()).onNext(any(Integer.class)); - verify(o, never()).onCompleted(); - verify(o).onError(any(IllegalStateException.class)); - } - @Test - public void testEmpty() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onCompleted(); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - aos.toObservable().subscribe(o); - - verify(o, never()).onNext(any(Integer.class)); - verify(o, never()).onError(any(Throwable.class)); - verify(o).onCompleted(); - } - @Test - public void testNever() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.stop(); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - aos.toObservable().subscribe(o); - - verify(o, never()).onNext(any(Integer.class)); - verify(o, never()).onError(any(Throwable.class)); - verify(o, never()).onCompleted(); - } - - @Test - public void testThrows() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - throw new TestException("Forced failure"); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - aos.toObservable().subscribe(o); - - verify(o, never()).onNext(any(Integer.class)); - verify(o, never()).onCompleted(); - verify(o).onError(any(TestException.class)); - } - - @Test - public void testError() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onError(new TestException("Forced failure")); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - aos.toObservable().subscribe(o); - - verify(o, never()).onNext(any(Integer.class)); - verify(o).onError(any(TestException.class)); - verify(o, never()).onCompleted(); - } - @Test - public void testRange() { - final int start = 1; - final int count = 100; - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - long calls = state.calls(); - if (calls <= count) { - state.onNext((int)calls + start); - if (calls == count) { - state.onCompleted(); - } - } - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - aos.toObservable().subscribe(o); - - verify(o, never()).onError(any(TestException.class)); - for (int i = start; i < start + count; i++) { - inOrder.verify(o).onNext(i); - } - inOrder.verify(o).onCompleted(); - inOrder.verifyNoMoreInteractions(); - } - @Test - public void testFromIterable() { - int n = 100; - final List source = new ArrayList(); - for (int i = 0; i < n; i++) { - source.add(i); - } - - AbstractOnSubscribe> aos = new AbstractOnSubscribe>() { - @Override - protected Iterator onSubscribe( - Subscriber subscriber) { - return source.iterator(); - } - @Override - protected void next(SubscriptionState> state) { - Iterator it = state.state(); - if (it.hasNext()) { - state.onNext(it.next()); - } - if (!it.hasNext()) { - state.onCompleted(); - } - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - aos.toObservable().subscribe(o); - - verify(o, never()).onError(any(TestException.class)); - for (int i = 0; i < n; i++) { - inOrder.verify(o).onNext(i); - } - inOrder.verify(o).onCompleted(); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void testPhased() { - final int count = 100; - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - long c = state.calls(); - switch (state.phase()) { - case 0: - if (c < count) { - state.onNext("Beginning"); - if (c == count - 1) { - state.advancePhase(); - } - } - break; - case 1: - state.onNext("Beginning"); - state.advancePhase(); - break; - case 2: - state.onNext("Finally"); - state.onCompleted(); - state.advancePhase(); - break; - default: - throw new IllegalStateException("Wrong phase: " + state.phase()); - } - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - aos.toObservable().subscribe(o); - - verify(o, never()).onError(any(Throwable.class)); - inOrder.verify(o, times(count + 1)).onNext("Beginning"); - inOrder.verify(o).onNext("Finally"); - inOrder.verify(o).onCompleted(); - inOrder.verifyNoMoreInteractions(); - } - @Test - public void testPhasedRetry() { - final int count = 100; - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - int calls; - int phase; - @Override - protected void next(SubscriptionState state) { - switch (phase) { - case 0: - if (calls++ < count) { - state.onNext("Beginning"); - state.onError(new TestException()); - } else { - phase++; - } - break; - case 1: - state.onNext("Beginning"); - phase++; - break; - case 2: - state.onNext("Finally"); - state.onCompleted(); - phase++; - break; - default: - throw new IllegalStateException("Wrong phase: " + state.phase()); - } - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - aos.toObservable().retry(2 * count).subscribe(o); - - verify(o, never()).onError(any(Throwable.class)); - inOrder.verify(o, times(count + 1)).onNext("Beginning"); - inOrder.verify(o).onNext("Finally"); - inOrder.verify(o).onCompleted(); - inOrder.verifyNoMoreInteractions(); - } - @Test - public void testInfiniteTake() { - int count = 100; - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onNext((int)state.calls()); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - aos.toObservable().take(count).subscribe(o); - - verify(o, never()).onError(any(Throwable.class)); - for (int i = 0; i < 100; i++) { - inOrder.verify(o).onNext(i); - } - inOrder.verify(o).onCompleted(); - inOrder.verifyNoMoreInteractions(); - } - @Test - public void testInfiniteRequestSome() { - int count = 100; - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onNext((int)state.calls()); - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - TestSubscriber ts = new TestSubscriber(o) { - @Override - public void onStart() { - requestMore(0); // don't start right away - } - }; - - aos.toObservable().subscribe(ts); - - ts.requestMore(count); - - verify(o, never()).onError(any(Throwable.class)); - verify(o, never()).onCompleted(); - for (int i = 0; i < count; i++) { - inOrder.verify(o).onNext(i); - } - inOrder.verifyNoMoreInteractions(); - } - @Test - public void testIndependentStates() { - int count = 100; - final ConcurrentHashMap states = new ConcurrentHashMap(); - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - states.put(state, state); - state.stop(); - } - }; - Observable source = aos.toObservable(); - for (int i = 0; i < count; i++) { - source.subscribe(); - } - - assertEquals(count, states.size()); - } - @Test(timeout = 3000) - public void testSubscribeOn() { - final int start = 1; - final int count = 100; - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - long calls = state.calls(); - if (calls <= count) { - state.onNext((int)calls + start); - if (calls == count) { - state.onCompleted(); - } - } - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - InOrder inOrder = inOrder(o); - - TestSubscriber ts = new TestSubscriber(o); - - aos.toObservable().subscribeOn(Schedulers.newThread()).subscribe(ts); - - ts.awaitTerminalEvent(); - - verify(o, never()).onError(any(Throwable.class)); - for (int i = 1; i <= count; i++) { - inOrder.verify(o).onNext(i); - } - inOrder.verify(o).onCompleted(); - inOrder.verifyNoMoreInteractions(); - - } - @Test(timeout = 10000) - public void testObserveOn() { - final int start = 1; - final int count = 1000; - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - long calls = state.calls(); - if (calls <= count) { - state.onNext((int)calls + start); - if (calls == count) { - state.onCompleted(); - } - } - } - }; - - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - TestSubscriber ts = new TestSubscriber(o); - - aos.toObservable().observeOn(Schedulers.newThread()).subscribe(ts); - - ts.awaitTerminalEvent(); - - verify(o, never()).onError(any(Throwable.class)); - verify(o, times(count + 1)).onNext(any(Integer.class)); - verify(o).onCompleted(); - - for (int i = 0; i < ts.getOnNextEvents().size(); i++) { - Object object = ts.getOnNextEvents().get(i); - assertEquals(i + 1, object); - } - } - @Test - public void testMissingEmission() { - @SuppressWarnings("unchecked") - Observer o = mock(Observer.class); - - Action1> empty = Actions.empty(); - AbstractOnSubscribe.create(empty).toObservable().subscribe(o); - - verify(o, never()).onCompleted(); - verify(o, never()).onNext(any(Object.class)); - verify(o).onError(any(IllegalStateException.class)); - } - - @Test - public void testCanRequestInOnNext() { - AbstractOnSubscribe aos = new AbstractOnSubscribe() { - @Override - protected void next(SubscriptionState state) { - state.onNext(1); - state.onCompleted(); - } - }; - final AtomicReference exception = new AtomicReference(); - aos.toObservable().subscribe(new Subscriber() { - - @Override - public void onCompleted() { - - } - - @Override - public void onError(Throwable e) { - exception.set(e); - } - - @Override - public void onNext(Integer t) { - request(1); - } - }); - if (exception.get()!=null) { - exception.get().printStackTrace(); - } - assertNull(exception.get()); - } -} diff --git a/src/test/java/rx/subjects/AsyncSubjectTest.java b/src/test/java/rx/subjects/AsyncSubjectTest.java index 623cdceb3f..968e71f571 100644 --- a/src/test/java/rx/subjects/AsyncSubjectTest.java +++ b/src/test/java/rx/subjects/AsyncSubjectTest.java @@ -15,11 +15,7 @@ */ package rx.subjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.inOrder; @@ -395,4 +391,43 @@ public void testCurrentStateMethodsError() { assertNull(as.getValue()); assertTrue(as.getThrowable() instanceof TestException); } + + @Test + public void testAsyncSubjectValueRelay() { + AsyncSubject async = AsyncSubject.create(); + async.onNext(1); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + } + @Test + public void testAsyncSubjectValueEmpty() { + AsyncSubject async = AsyncSubject.create(); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + @Test + public void testAsyncSubjectValueError() { + AsyncSubject async = AsyncSubject.create(); + TestException te = new TestException(); + async.onError(te); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertTrue(async.hasThrowable()); + assertSame(te, async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } } diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index 9e9e4c90e7..bd3d7da58f 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -15,11 +15,7 @@ */ package rx.subjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -569,4 +565,86 @@ public void testCurrentStateMethodsError() { assertNull(as.getValue()); assertTrue(as.getThrowable() instanceof TestException); } + + @Test + public void testBehaviorSubjectValueRelay() { + BehaviorSubject async = BehaviorSubject.create(); + async.onNext(1); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectValueRelayIncomplete() { + BehaviorSubject async = BehaviorSubject.create(); + async.onNext(1); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectIncompleteEmpty() { + BehaviorSubject async = BehaviorSubject.create(); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectEmpty() { + BehaviorSubject async = BehaviorSubject.create(); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testBehaviorSubjectError() { + BehaviorSubject async = BehaviorSubject.create(); + TestException te = new TestException(); + async.onError(te); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertTrue(async.hasThrowable()); + assertSame(te, async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } } diff --git a/src/test/java/rx/subjects/PublishSubjectTest.java b/src/test/java/rx/subjects/PublishSubjectTest.java index 44fe824a5c..7b3248d8d7 100644 --- a/src/test/java/rx/subjects/PublishSubjectTest.java +++ b/src/test/java/rx/subjects/PublishSubjectTest.java @@ -15,11 +15,7 @@ */ package rx.subjects; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -447,4 +443,38 @@ public void testCurrentStateMethodsError() { assertFalse(as.hasCompleted()); assertTrue(as.getThrowable() instanceof TestException); } + + @Test + public void testPublishSubjectValueRelay() { + PublishSubject async = PublishSubject.create(); + async.onNext(1); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + } + + @Test + public void testPublishSubjectValueEmpty() { + PublishSubject async = PublishSubject.create(); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + } + @Test + public void testPublishSubjectValueError() { + PublishSubject async = PublishSubject.create(); + TestException te = new TestException(); + async.onError(te); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertTrue(async.hasThrowable()); + assertSame(te, async.getThrowable()); + } } diff --git a/src/test/java/rx/subjects/ReplaySubjectTest.java b/src/test/java/rx/subjects/ReplaySubjectTest.java index 5ebb871604..cd04fc02cc 100644 --- a/src/test/java/rx/subjects/ReplaySubjectTest.java +++ b/src/test/java/rx/subjects/ReplaySubjectTest.java @@ -15,12 +15,7 @@ */ package rx.subjects; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -887,4 +882,171 @@ public void testGetValuesUnbounded() { assertArrayEquals(expected, rs.getValues()); } + + @Test + public void testReplaySubjectValueRelay() { + ReplaySubject async = ReplaySubject.create(); + async.onNext(1); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayIncomplete() { + ReplaySubject async = ReplaySubject.create(); + async.onNext(1); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayBounded() { + ReplaySubject async = ReplaySubject.createWithSize(1); + async.onNext(0); + async.onNext(1); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayBoundedIncomplete() { + ReplaySubject async = ReplaySubject.createWithSize(1); + async.onNext(0); + async.onNext(1); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayBoundedEmptyIncomplete() { + ReplaySubject async = ReplaySubject.createWithSize(1); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectValueRelayEmptyIncomplete() { + ReplaySubject async = ReplaySubject.create(); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testReplaySubjectEmpty() { + ReplaySubject async = ReplaySubject.create(); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectError() { + ReplaySubject async = ReplaySubject.create(); + TestException te = new TestException(); + async.onError(te); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertTrue(async.hasThrowable()); + assertSame(te, async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void testReplaySubjectBoundedEmpty() { + ReplaySubject async = ReplaySubject.createWithSize(1); + async.onCompleted(); + + assertFalse(async.hasObservers()); + assertTrue(async.hasCompleted()); + assertFalse(async.hasThrowable()); + assertNull(async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + @Test + public void testReplaySubjectBoundedError() { + ReplaySubject async = ReplaySubject.createWithSize(1); + TestException te = new TestException(); + async.onError(te); + + assertFalse(async.hasObservers()); + assertFalse(async.hasCompleted()); + assertTrue(async.hasThrowable()); + assertSame(te, async.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } } diff --git a/src/test/java/rx/subjects/SerializedSubjectTest.java b/src/test/java/rx/subjects/SerializedSubjectTest.java index 097fcd311e..b31a458ffd 100644 --- a/src/test/java/rx/subjects/SerializedSubjectTest.java +++ b/src/test/java/rx/subjects/SerializedSubjectTest.java @@ -15,13 +15,12 @@ */ package rx.subjects; -import static org.junit.Assert.*; +import static org.junit.Assert.assertSame; import java.util.Arrays; import org.junit.Test; -import rx.exceptions.TestException; import rx.observers.TestSubscriber; public class SerializedSubjectTest { @@ -37,379 +36,6 @@ public void testBasic() { ts.assertReceivedOnNext(Arrays.asList("hello")); } - @Test - public void testAsyncSubjectValueRelay() { - AsyncSubject async = AsyncSubject.create(); - async.onNext(1); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertEquals((Integer)1, serial.getValue()); - assertTrue(serial.hasValue()); - assertArrayEquals(new Object[] { 1 }, serial.getValues()); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testAsyncSubjectValueEmpty() { - AsyncSubject async = AsyncSubject.create(); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testAsyncSubjectValueError() { - AsyncSubject async = AsyncSubject.create(); - TestException te = new TestException(); - async.onError(te); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertTrue(serial.hasThrowable()); - assertSame(te, serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testPublishSubjectValueRelay() { - PublishSubject async = PublishSubject.create(); - async.onNext(1); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - - assertArrayEquals(new Object[0], serial.getValues()); - assertArrayEquals(new Integer[0], serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - - @Test - public void testPublishSubjectValueEmpty() { - PublishSubject async = PublishSubject.create(); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testPublishSubjectValueError() { - PublishSubject async = PublishSubject.create(); - TestException te = new TestException(); - async.onError(te); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertTrue(serial.hasThrowable()); - assertSame(te, serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - - @Test - public void testBehaviorSubjectValueRelay() { - BehaviorSubject async = BehaviorSubject.create(); - async.onNext(1); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testBehaviorSubjectValueRelayIncomplete() { - BehaviorSubject async = BehaviorSubject.create(); - async.onNext(1); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertEquals((Integer)1, serial.getValue()); - assertTrue(serial.hasValue()); - assertArrayEquals(new Object[] { 1 }, serial.getValues()); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testBehaviorSubjectIncompleteEmpty() { - BehaviorSubject async = BehaviorSubject.create(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testBehaviorSubjectEmpty() { - BehaviorSubject async = BehaviorSubject.create(); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testBehaviorSubjectError() { - BehaviorSubject async = BehaviorSubject.create(); - TestException te = new TestException(); - async.onError(te); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertTrue(serial.hasThrowable()); - assertSame(te, serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - - @Test - public void testReplaySubjectValueRelay() { - ReplaySubject async = ReplaySubject.create(); - async.onNext(1); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertEquals((Integer)1, serial.getValue()); - assertTrue(serial.hasValue()); - assertArrayEquals(new Object[] { 1 }, serial.getValues()); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testReplaySubjectValueRelayIncomplete() { - ReplaySubject async = ReplaySubject.create(); - async.onNext(1); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertEquals((Integer)1, serial.getValue()); - assertTrue(serial.hasValue()); - assertArrayEquals(new Object[] { 1 }, serial.getValues()); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testReplaySubjectValueRelayBounded() { - ReplaySubject async = ReplaySubject.createWithSize(1); - async.onNext(0); - async.onNext(1); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertEquals((Integer)1, serial.getValue()); - assertTrue(serial.hasValue()); - assertArrayEquals(new Object[] { 1 }, serial.getValues()); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testReplaySubjectValueRelayBoundedIncomplete() { - ReplaySubject async = ReplaySubject.createWithSize(1); - async.onNext(0); - async.onNext(1); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertEquals((Integer)1, serial.getValue()); - assertTrue(serial.hasValue()); - assertArrayEquals(new Object[] { 1 }, serial.getValues()); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { 1 }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { 1, null }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testReplaySubjectValueRelayBoundedEmptyIncomplete() { - ReplaySubject async = ReplaySubject.createWithSize(1); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testReplaySubjectValueRelayEmptyIncomplete() { - ReplaySubject async = ReplaySubject.create(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - - @Test - public void testReplaySubjectEmpty() { - ReplaySubject async = ReplaySubject.create(); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testReplaySubjectError() { - ReplaySubject async = ReplaySubject.create(); - TestException te = new TestException(); - async.onError(te); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertTrue(serial.hasThrowable()); - assertSame(te, serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - - @Test - public void testReplaySubjectBoundedEmpty() { - ReplaySubject async = ReplaySubject.createWithSize(1); - async.onCompleted(); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertTrue(serial.hasCompleted()); - assertFalse(serial.hasThrowable()); - assertNull(serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test - public void testReplaySubjectBoundedError() { - ReplaySubject async = ReplaySubject.createWithSize(1); - TestException te = new TestException(); - async.onError(te); - Subject serial = async.toSerialized(); - - assertFalse(serial.hasObservers()); - assertFalse(serial.hasCompleted()); - assertTrue(serial.hasThrowable()); - assertSame(te, serial.getThrowable()); - assertNull(serial.getValue()); - assertFalse(serial.hasValue()); - assertArrayEquals(new Object[] { }, serial.getValues()); - assertArrayEquals(new Integer[] { }, serial.getValues(new Integer[0])); - assertArrayEquals(new Integer[] { null }, serial.getValues(new Integer[] { 0 })); - assertArrayEquals(new Integer[] { null, 0 }, serial.getValues(new Integer[] { 0, 0 })); - } - @Test public void testDontWrapSerializedSubjectAgain() { PublishSubject s = PublishSubject.create(); From f203ae2aa1e21c39767980d35d382f61a2650eb5 Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Tue, 1 Dec 2015 14:03:43 -0800 Subject: [PATCH 455/857] Update CHANGES.md for v1.0.17 --- CHANGES.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 5fa01c46ec..b0b16a730c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,16 @@ # RxJava Releases # +### Version 1.0.17 – December 1 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.17%7C)) ### + +* [Pull 3491] (https://github.com/ReactiveX/RxJava/pull/3491) Make scan's delayed Producer independent of event serialization +* [Pull 3150] (https://github.com/ReactiveX/RxJava/pull/3150) Window operators now support backpressure in the inner observable +* [Pull 3535] (https://github.com/ReactiveX/RxJava/pull/3535) Don't swallow fatal errors in OperatorZipIterable +* [Pull 3528] (https://github.com/ReactiveX/RxJava/pull/3528) Avoid to call next when Iterator is drained +* [Pull 3436] (https://github.com/ReactiveX/RxJava/pull/3436) Add action != null check in OperatorFinally +* [Pull 3513] (https://github.com/ReactiveX/RxJava/pull/3513) Add shorter RxJavaPlugin class lookup approach + ### Version 1.0.16 – November 11 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.16%7C)) ### + * [Pull 3169] (https://github.com/ReactiveX/RxJava/pull/3169) Merge can now operate in horizontally unbounded mode * [Pull 3286] (https://github.com/ReactiveX/RxJava/pull/3286) Implements BlockingSingle * [Pull 3433] (https://github.com/ReactiveX/RxJava/pull/3433) Add Single.defer() From 99fff99a912f052af3552248308419280bd13b5d Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Wed, 2 Dec 2015 18:17:00 -0800 Subject: [PATCH 456/857] Update CHANGES.md for v1.1.0 --- CHANGES.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b0b16a730c..3d60beeddf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,36 @@ # RxJava Releases # +### Version 1.1.0 – December 2 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.1.0%7C)) ### + +* [Pull 3550] (https://github.com/ReactiveX/RxJava/pull/3550) Public API changes for 1.1.0 release + +#### Promotions to Public API + +* Subscriptions.unsubscribed +* Subscribers.wrap +* 2 RxJavaErrorHandler methods +* Single + SingleSubscriber +* Exceptions.throwIfAny +* Observable.switchIfEmpty with Observable +* BackpressureDrainManager +* Observable.onBackpressureLatest +* Observable.onBackpressureDrop with action +* Observable.onBackpressureBuffer overloads +* 2 Observable.merge overloads for maxConcurrent +* TestSubscriber methods +* Observable.takeUntil with predicate + +#### Promotions to BETA + +* ConnectableObservable.autoConnect +* Stateful Subject methods on ReplaySubject, PublishSubject, BehaviorSubject, and AsyncSubject + +#### Removals from Public API + +* Observable.onBackpressureBlock +* rx.observables.AbstractOnSubscribe +* Removal of stateful methods from the generic rx.subjects.Subject abstract class + ### Version 1.0.17 – December 1 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.0.17%7C)) ### * [Pull 3491] (https://github.com/ReactiveX/RxJava/pull/3491) Make scan's delayed Producer independent of event serialization From 4949ee33e21b67e728f90b24d25ab762b901e4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 3 Dec 2015 10:35:05 +0100 Subject: [PATCH 457/857] 1.x: fix toMap and toMultimap not handling exceptions of the callbacks This PR adds the usual try-catch around callback invocations in `toMap` and `toMultimap`. Related #3555. --- .../rx/internal/operators/OperatorToMap.java | 33 ++++++- .../operators/OperatorToMultimap.java | 42 +++++++- .../internal/operators/OperatorToMapTest.java | 81 +++++++++++++--- .../operators/OperatorToMultimapTest.java | 96 +++++++++++++++++++ 4 files changed, 233 insertions(+), 19 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorToMap.java b/src/main/java/rx/internal/operators/OperatorToMap.java index 97decaa6da..5b81e071fb 100644 --- a/src/main/java/rx/internal/operators/OperatorToMap.java +++ b/src/main/java/rx/internal/operators/OperatorToMap.java @@ -21,8 +21,10 @@ import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.functions.Func0; import rx.functions.Func1; +import rx.observers.Subscribers; /** * Maps the elements of the source observable into a java.util.Map instance and @@ -75,9 +77,24 @@ public OperatorToMap( @Override public Subscriber call(final Subscriber> subscriber) { + + Map localMap; + + try { + localMap = mapFactory.call(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + subscriber.onError(ex); + Subscriber parent = Subscribers.empty(); + parent.unsubscribe(); + return parent; + } + + final Map fLocalMap = localMap; + return new Subscriber(subscriber) { - private Map map = mapFactory.call(); + private Map map = fLocalMap; @Override public void onStart() { @@ -86,8 +103,18 @@ public void onStart() { @Override public void onNext(T v) { - K key = keySelector.call(v); - V value = valueSelector.call(v); + K key; + V value; + + try { + key = keySelector.call(v); + value = valueSelector.call(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + subscriber.onError(ex); + return; + } + map.put(key, value); } diff --git a/src/main/java/rx/internal/operators/OperatorToMultimap.java b/src/main/java/rx/internal/operators/OperatorToMultimap.java index f7b998ed94..6b840bed18 100644 --- a/src/main/java/rx/internal/operators/OperatorToMultimap.java +++ b/src/main/java/rx/internal/operators/OperatorToMultimap.java @@ -22,9 +22,11 @@ import java.util.Map; import rx.Observable.Operator; +import rx.exceptions.Exceptions; import rx.Subscriber; import rx.functions.Func0; import rx.functions.Func1; +import rx.observers.Subscribers; /** * Maps the elements of the source observable into a multimap @@ -103,8 +105,24 @@ public OperatorToMultimap( @Override public Subscriber call(final Subscriber>> subscriber) { + + Map> localMap; + + try { + localMap = mapFactory.call(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + subscriber.onError(ex); + + Subscriber parent = Subscribers.empty(); + parent.unsubscribe(); + return parent; + } + + final Map> fLocalMap = localMap; + return new Subscriber(subscriber) { - private Map> map = mapFactory.call(); + private Map> map = fLocalMap; @Override public void onStart() { @@ -113,11 +131,27 @@ public void onStart() { @Override public void onNext(T v) { - K key = keySelector.call(v); - V value = valueSelector.call(v); + K key; + V value; + + try { + key = keySelector.call(v); + value = valueSelector.call(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + subscriber.onError(ex); + return; + } + Collection collection = map.get(key); if (collection == null) { - collection = collectionFactory.call(key); + try { + collection = collectionFactory.call(key); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + subscriber.onError(ex); + return; + } map.put(key, collection); } collection.add(value); diff --git a/src/test/java/rx/internal/operators/OperatorToMapTest.java b/src/test/java/rx/internal/operators/OperatorToMapTest.java index 669b85c234..466cff0df8 100644 --- a/src/test/java/rx/internal/operators/OperatorToMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorToMapTest.java @@ -16,24 +16,19 @@ package rx.internal.operators; import static org.mockito.Matchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.junit.*; +import org.mockito.*; import rx.Observable; import rx.Observer; -import rx.functions.Func0; -import rx.functions.Func1; +import rx.exceptions.TestException; +import rx.functions.*; import rx.internal.util.UtilityFunctions; +import rx.observers.TestSubscriber; public class OperatorToMapTest { @Mock @@ -224,4 +219,66 @@ public Integer call(String t1) { verify(objectObserver, times(1)).onError(any(Throwable.class)); } + @Test + public void testKeySelectorThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1, 2).toMap(new Func1() { + @Override + public Integer call(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertError(TestException.class); + ts.assertNoValues(); + ts.assertNotCompleted(); + } + + @Test + public void testValueSelectorThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1, 2).toMap(new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func1() { + @Override + public Integer call(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertError(TestException.class); + ts.assertNoValues(); + ts.assertNotCompleted(); + } + + @Test + public void testMapFactoryThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1, 2).toMap(new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func0>() { + @Override + public Map call() { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertError(TestException.class); + ts.assertNoValues(); + ts.assertNotCompleted(); + } } diff --git a/src/test/java/rx/internal/operators/OperatorToMultimapTest.java b/src/test/java/rx/internal/operators/OperatorToMultimapTest.java index b8f57f04f6..f93f57500d 100644 --- a/src/test/java/rx/internal/operators/OperatorToMultimapTest.java +++ b/src/test/java/rx/internal/operators/OperatorToMultimapTest.java @@ -36,11 +36,13 @@ import rx.Observable; import rx.Observer; +import rx.exceptions.TestException; import rx.functions.Func0; import rx.functions.Func1; import rx.internal.operators.OperatorToMultimap.DefaultMultimapCollectionFactory; import rx.internal.operators.OperatorToMultimap.DefaultToMultimapFactory; import rx.internal.util.UtilityFunctions; +import rx.observers.TestSubscriber; public class OperatorToMultimapTest { @Mock @@ -269,4 +271,98 @@ public Collection call(Integer t1) { verify(objectObserver, never()).onNext(expected); verify(objectObserver, never()).onCompleted(); } + + @Test + public void testKeySelectorThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1, 2).toMultimap(new Func1() { + @Override + public Integer call(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertError(TestException.class); + ts.assertNoValues(); + ts.assertNotCompleted(); + } + + @Test + public void testValueSelectorThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1, 2).toMultimap(new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func1() { + @Override + public Integer call(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertError(TestException.class); + ts.assertNoValues(); + ts.assertNotCompleted(); + } + + @Test + public void testMapFactoryThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1, 2).toMultimap(new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func0>>() { + @Override + public Map> call() { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertError(TestException.class); + ts.assertNoValues(); + ts.assertNotCompleted(); + } + + @Test + public void testCollectionFactoryThrows() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1, 2).toMultimap(new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func1() { + @Override + public Integer call(Integer v) { + return v; + } + }, new Func0>>() { + @Override + public Map> call() { + return new HashMap>(); + } + }, new Func1>() { + @Override + public Collection call(Integer k) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertError(TestException.class); + ts.assertNoValues(); + ts.assertNotCompleted(); + } } From 5eaeb1af613ba7a200b97cd5a3fc5f502dbb1e1f Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Fri, 4 Dec 2015 15:09:33 -0800 Subject: [PATCH 458/857] Rewording 1.1.0 release notes in CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3d60beeddf..2187b97999 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,7 +25,7 @@ * ConnectableObservable.autoConnect * Stateful Subject methods on ReplaySubject, PublishSubject, BehaviorSubject, and AsyncSubject -#### Removals from Public API +#### Experimental APIs Removed * Observable.onBackpressureBlock * rx.observables.AbstractOnSubscribe From ebad70de2750c7fc72a217a6011c9d2a452ab198 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sat, 5 Dec 2015 22:15:51 +0300 Subject: [PATCH 459/857] Add Single.doOnUnsubscribe() --- src/main/java/rx/Single.java | 22 +++++++++++ src/test/java/rx/SingleTest.java | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 190e1630b3..b577f9a812 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -21,6 +21,7 @@ import rx.annotations.Experimental; import rx.exceptions.Exceptions; import rx.exceptions.OnErrorNotImplementedException; +import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; @@ -34,6 +35,7 @@ import rx.internal.operators.OnSubscribeToObservableFuture; import rx.internal.operators.OperatorDelay; import rx.internal.operators.OperatorDoOnEach; +import rx.internal.operators.OperatorDoOnUnsubscribe; import rx.internal.operators.OperatorMap; import rx.internal.operators.OperatorObserveOn; import rx.internal.operators.OperatorOnErrorReturn; @@ -1998,4 +2000,24 @@ public void call(SingleSubscriber singleSubscriber) { } }); } + + /** + * Modifies the source {@link Single} so that it invokes the given action when it is unsubscribed from + * its subscribers. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code doOnUnsubscribe} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param action + * the action that gets called when this {@link Single} is unsubscribed. + * @return the source {@link Single} modified so as to call this Action when appropriate. + * @see ReactiveX operators documentation: Do + */ + @Experimental + public final Single doOnUnsubscribe(final Action0 action) { + return lift(new OperatorDoOnUnsubscribe(action)); + } } diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 30fe99e92f..0fa8750709 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -40,6 +40,7 @@ import org.mockito.stubbing.Answer; import rx.Single.OnSubscribe; import rx.exceptions.CompositeException; +import rx.functions.Action; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; @@ -828,4 +829,66 @@ public void deferShouldPassNullPointerExceptionToTheSubscriberIfSingleFactoryRet verify(singleFactory).call(); } + + @Test + public void doOnUnsubscribeShouldInvokeActionAfterSuccess() { + Action0 action = mock(Action0.class); + + Single single = Single + .just("test") + .doOnUnsubscribe(action); + + verifyZeroInteractions(action); + + TestSubscriber testSubscriber = new TestSubscriber(); + single.subscribe(testSubscriber); + + testSubscriber.assertValue("test"); + testSubscriber.assertCompleted(); + + verify(action).call(); + } + + @Test + public void doOnUnsubscribeShouldInvokeActionAfterError() { + Action0 action = mock(Action0.class); + + Single single = Single + .error(new RuntimeException("test")) + .doOnUnsubscribe(action); + + verifyZeroInteractions(action); + + TestSubscriber testSubscriber = new TestSubscriber(); + single.subscribe(testSubscriber); + + testSubscriber.assertError(RuntimeException.class); + assertEquals("test", testSubscriber.getOnErrorEvents().get(0).getMessage()); + + verify(action).call(); + } + + @Test + public void doOnUnsubscribeShouldInvokeActionAfterExplicitUnsubscription() { + Action0 action = mock(Action0.class); + + Single single = Single + .create(new OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + // Broken Single that never ends itself (simulates long computation in one thread). + } + }) + .doOnUnsubscribe(action); + + TestSubscriber testSubscriber = new TestSubscriber(); + Subscription subscription = single.subscribe(testSubscriber); + + verifyZeroInteractions(action); + + subscription.unsubscribe(); + verify(action).call(); + testSubscriber.assertNoValues(); + testSubscriber.assertNoTerminalEvent(); + } } From 89b2e9fc230775e39362248653eaca66a408d2b3 Mon Sep 17 00:00:00 2001 From: Sebas LG Date: Sun, 6 Dec 2015 18:35:04 +0100 Subject: [PATCH 460/857] Fix typo in documentation --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 3b9f404e08..04551d98af 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -1252,7 +1252,7 @@ public final static Observable from(T[] array) { *

    * *

    - * This allows you to defer the execution of the function you specify untl an observer subscribes to the + * This allows you to defer the execution of the function you specify until an observer subscribes to the * Observable. That is to say, it makes the function "lazy." *

    *
    Scheduler:
    From 8eb76715a1a4a7cabc56a17c0d44bc6bb9d2ebcf Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Sat, 10 Oct 2015 19:44:55 +0300 Subject: [PATCH 461/857] Add Single.doAfterTerminate() --- src/main/java/rx/Single.java | 22 ++++++++++++++ src/test/java/rx/SingleTest.java | 52 +++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index b577f9a812..3880da9b0c 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -36,6 +36,7 @@ import rx.internal.operators.OperatorDelay; import rx.internal.operators.OperatorDoOnEach; import rx.internal.operators.OperatorDoOnUnsubscribe; +import rx.internal.operators.OperatorFinally; import rx.internal.operators.OperatorMap; import rx.internal.operators.OperatorObserveOn; import rx.internal.operators.OperatorOnErrorReturn; @@ -2020,4 +2021,25 @@ public void call(SingleSubscriber singleSubscriber) { public final Single doOnUnsubscribe(final Action0 action) { return lift(new OperatorDoOnUnsubscribe(action)); } + + /** + * Registers an {@link Action0} to be called when this {@link Single} invokes either + * {@link SingleSubscriber#onSuccess(Object)} onSuccess} or {@link SingleSubscriber#onError onError}. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code doAfterTerminate} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param action + * an {@link Action0} to be invoked when the source {@link Single} finishes. + * @return a {@link Single} that emits the same item or error as the source {@link Single}, then invokes the + * {@link Action0} + * @see ReactiveX operators documentation: Do + */ + @Experimental + public final Single doAfterTerminate(Action0 action) { + return lift(new OperatorFinally(action)); + } } diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 0fa8750709..17e3367835 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -51,7 +51,6 @@ import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; - public class SingleTest { @Test @@ -891,4 +890,55 @@ public void call(SingleSubscriber singleSubscriber) { testSubscriber.assertNoValues(); testSubscriber.assertNoTerminalEvent(); } + + @Test + public void doAfterTerminateActionShouldBeInvokedAfterOnSuccess() { + Action0 action = mock(Action0.class); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .just("value") + .doAfterTerminate(action) + .subscribe(testSubscriber); + + testSubscriber.assertValue("value"); + testSubscriber.assertNoErrors(); + + verify(action).call(); + } + + @Test + public void doAfterTerminateActionShouldBeInvokedAfterOnError() { + Action0 action = mock(Action0.class); + + TestSubscriber testSubscriber = new TestSubscriber(); + + Throwable error = new IllegalStateException(); + + Single + .error(error) + .doAfterTerminate(action) + .subscribe(testSubscriber); + + testSubscriber.assertNoValues(); + testSubscriber.assertError(error); + + verify(action).call(); + } + + @Test + public void doAfterTerminateActionShouldNotBeInvokedUntilSubscriberSubscribes() { + Action0 action = mock(Action0.class); + + Single + .just("value") + .doAfterTerminate(action); + + Single + .error(new IllegalStateException()) + .doAfterTerminate(action); + + verifyZeroInteractions(action); + } } From 60769b37471d1ff22ac217ecb3769438485ef24e Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Wed, 9 Dec 2015 04:38:19 +0300 Subject: [PATCH 462/857] Deprecate Observable.finallyDo() and add Observable.doAfterTerminate() instead --- src/main/java/rx/Observable.java | 25 ++++++++++++++++++- ...lly.java => OperatorDoAfterTerminate.java} | 4 +-- ...java => OperatorDoAfterTerminateTest.java} | 10 ++++---- .../operators/OperatorObserveOnTest.java | 2 +- 4 files changed, 32 insertions(+), 9 deletions(-) rename src/main/java/rx/internal/operators/{OperatorFinally.java => OperatorDoAfterTerminate.java} (93%) rename src/test/java/rx/internal/operators/{OperatorFinallyTest.java => OperatorDoAfterTerminateTest.java} (86%) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 04551d98af..59cdd4f3f9 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5089,9 +5089,32 @@ public final Observable filter(Func1 predicate) { * {@link Action0} * @see ReactiveX operators documentation: Do * @see #doOnTerminate(Action0) + * @deprecated use {@link #doAfterTerminate(Action0)} instead. */ + @Deprecated public final Observable finallyDo(Action0 action) { - return lift(new OperatorFinally(action)); + return lift(new OperatorDoAfterTerminate(action)); + } + + /** + * Registers an {@link Action0} to be called when this Observable invokes either + * {@link Observer#onCompleted onCompleted} or {@link Observer#onError onError}. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code doAfterTerminate} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param action + * an {@link Action0} to be invoked when the source Observable finishes + * @return an Observable that emits the same items as the source Observable, then invokes the + * {@link Action0} + * @see ReactiveX operators documentation: Do + * @see #doOnTerminate(Action0) + */ + public final Observable doAfterTerminate(Action0 action) { + return lift(new OperatorDoAfterTerminate(action)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorFinally.java b/src/main/java/rx/internal/operators/OperatorDoAfterTerminate.java similarity index 93% rename from src/main/java/rx/internal/operators/OperatorFinally.java rename to src/main/java/rx/internal/operators/OperatorDoAfterTerminate.java index 5f870f8f37..a56d28795c 100644 --- a/src/main/java/rx/internal/operators/OperatorFinally.java +++ b/src/main/java/rx/internal/operators/OperatorDoAfterTerminate.java @@ -29,10 +29,10 @@ * * @param the value type */ -public final class OperatorFinally implements Operator { +public final class OperatorDoAfterTerminate implements Operator { final Action0 action; - public OperatorFinally(Action0 action) { + public OperatorDoAfterTerminate(Action0 action) { if (action == null) { throw new NullPointerException("Action can not be null"); } diff --git a/src/test/java/rx/internal/operators/OperatorFinallyTest.java b/src/test/java/rx/internal/operators/OperatorDoAfterTerminateTest.java similarity index 86% rename from src/test/java/rx/internal/operators/OperatorFinallyTest.java rename to src/test/java/rx/internal/operators/OperatorDoAfterTerminateTest.java index e89ee74468..6295386ae1 100644 --- a/src/test/java/rx/internal/operators/OperatorFinallyTest.java +++ b/src/test/java/rx/internal/operators/OperatorDoAfterTerminateTest.java @@ -28,7 +28,7 @@ import rx.Observer; import rx.functions.Action0; -public class OperatorFinallyTest { +public class OperatorDoAfterTerminateTest { private Action0 aAction0; private Observer observer; @@ -42,24 +42,24 @@ public void before() { } private void checkActionCalled(Observable input) { - input.finallyDo(aAction0).subscribe(observer); + input.doAfterTerminate(aAction0).subscribe(observer); verify(aAction0, times(1)).call(); } @Test - public void testFinallyCalledOnComplete() { + public void testDoAfterTerminateCalledOnComplete() { checkActionCalled(Observable.from(new String[] { "1", "2", "3" })); } @Test - public void testFinallyCalledOnError() { + public void testDoAfterTerminateCalledOnError() { checkActionCalled(Observable. error(new RuntimeException("expected"))); } @Test public void nullActionShouldBeCheckedInConstructor() { try { - new OperatorFinally(null); + new OperatorDoAfterTerminate(null); fail(); } catch (NullPointerException expected) { assertEquals("Action can not be null", expected.getMessage()); diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index e505bf0672..65a4085384 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -138,7 +138,7 @@ public void call(String t1) { assertTrue(correctThreadName); } - }).finallyDo(new Action0() { + }).doAfterTerminate(new Action0() { @Override public void call() { From fa824fea2762ab39c0e66725694a0d81fbb1602f Mon Sep 17 00:00:00 2001 From: Niklas Baudy Date: Tue, 8 Dec 2015 17:37:04 +0100 Subject: [PATCH 463/857] Replace never() calls in BehaviorSubjectTest with verifyNoMoreInteractions Made testException final again --- .../java/rx/subjects/BehaviorSubjectTest.java | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/test/java/rx/subjects/BehaviorSubjectTest.java b/src/test/java/rx/subjects/BehaviorSubjectTest.java index 9e9e4c90e7..449aa4cc6f 100644 --- a/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; @@ -61,8 +62,7 @@ public void testThatObserverReceivesDefaultValueAndSubsequentEvents() { verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); - verify(observer, never()).onError(testException); - verify(observer, never()).onCompleted(); + verifyNoMoreInteractions(observer); } @Test @@ -78,12 +78,10 @@ public void testThatObserverReceivesLatestAndThenSubsequentEvents() { subject.onNext("two"); subject.onNext("three"); - verify(observer, never()).onNext("default"); verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); - verify(observer, never()).onError(testException); - verify(observer, never()).onCompleted(); + verifyNoMoreInteractions(observer); } @Test @@ -99,8 +97,8 @@ public void testSubscribeThenOnComplete() { verify(observer, times(1)).onNext("default"); verify(observer, times(1)).onNext("one"); - verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); + verifyNoMoreInteractions(observer); } @Test @@ -113,10 +111,8 @@ public void testSubscribeToCompletedOnlyEmitsOnComplete() { Observer observer = mock(Observer.class); subject.subscribe(observer); - verify(observer, never()).onNext("default"); - verify(observer, never()).onNext("one"); - verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); + verifyNoMoreInteractions(observer); } @Test @@ -130,10 +126,8 @@ public void testSubscribeToErrorOnlyEmitsOnError() { Observer observer = mock(Observer.class); subject.subscribe(observer); - verify(observer, never()).onNext("default"); - verify(observer, never()).onNext("one"); verify(observer, times(1)).onError(re); - verify(observer, never()).onCompleted(); + verifyNoMoreInteractions(observer); } @Test @@ -198,8 +192,7 @@ public void testCompletedAfterErrorIsNotSent() { verify(observer, times(1)).onNext("default"); verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onError(testException); - verify(observer, never()).onNext("two"); - verify(observer, never()).onCompleted(); + verifyNoMoreInteractions(observer); } @Test @@ -218,15 +211,13 @@ public void testCompletedAfterErrorIsNotSent2() { verify(observer, times(1)).onNext("default"); verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onError(testException); - verify(observer, never()).onNext("two"); - verify(observer, never()).onCompleted(); + verifyNoMoreInteractions(observer); @SuppressWarnings("unchecked") Observer o2 = mock(Observer.class); subject.subscribe(o2); verify(o2, times(1)).onError(testException); - verify(o2, never()).onNext(any()); - verify(o2, never()).onCompleted(); + verifyNoMoreInteractions(o2); } @Test @@ -245,15 +236,13 @@ public void testCompletedAfterErrorIsNotSent3() { verify(observer, times(1)).onNext("default"); verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onCompleted(); - verify(observer, never()).onError(any(Throwable.class)); - verify(observer, never()).onNext("two"); + verifyNoMoreInteractions(observer); @SuppressWarnings("unchecked") Observer o2 = mock(Observer.class); subject.subscribe(o2); verify(o2, times(1)).onCompleted(); - verify(o2, never()).onNext(any()); - verify(o2, never()).onError(any(Throwable.class)); + verifyNoMoreInteractions(o2); } @Test(timeout = 1000) public void testUnsubscriptionCase() { @@ -344,8 +333,7 @@ public void testStartEmptyCompleteWithOne() { source.subscribe(o); verify(o).onCompleted(); - verify(o, never()).onError(any(Throwable.class)); - verify(o, never()).onNext(any()); + verifyNoMoreInteractions(o); } @Test @@ -358,8 +346,8 @@ public void testTakeOneSubscriber() { verify(o).onNext(1); verify(o).onCompleted(); - verify(o, never()).onError(any(Throwable.class)); - + verifyNoMoreInteractions(o); + assertEquals(0, source.subscriberCount()); assertFalse(source.hasObservers()); } From 8646d8db4b624cb0ecc83a7874a156ee82e20b2f Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Tue, 8 Dec 2015 19:21:03 -0800 Subject: [PATCH 464/857] Renamed Completable#finallyDo to #doAfterTerminate --- src/main/java/rx/Completable.java | 2 +- src/test/java/rx/CompletableTest.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index 5fd7216a3b..8d53ff25d2 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -1356,7 +1356,7 @@ public final Observable endWith(Observable next) { * @return the new Completable instance * @throws NullPointerException if onAfterComplete is null */ - public final Completable finallyDo(Action0 onAfterComplete) { + public final Completable doAfterTerminate(Action0 onAfterComplete) { return doOnLifecycle(Actions.empty(), Actions.empty(), Actions.empty(), onAfterComplete, Actions.empty()); } diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index ec73ad0141..09d71a9ff7 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -1888,11 +1888,11 @@ public void call() { } @Test(timeout = 1000) - public void finallyDoNormal() { + public void doAfterTerminateNormal() { final AtomicBoolean doneAfter = new AtomicBoolean(); final AtomicBoolean complete = new AtomicBoolean(); - Completable c = normal.completable.finallyDo(new Action0() { + Completable c = normal.completable.doAfterTerminate(new Action0() { @Override public void call() { doneAfter.set(complete.get()); @@ -1919,14 +1919,14 @@ public void onCompleted() { c.await(); Assert.assertTrue("Not completed", complete.get()); - Assert.assertTrue("Finally called before onComplete", doneAfter.get()); + Assert.assertTrue("Closure called before onComplete", doneAfter.get()); } @Test(timeout = 1000) - public void finallyDoWithError() { + public void doAfterTerminateWithError() { final AtomicBoolean doneAfter = new AtomicBoolean(); - Completable c = error.completable.finallyDo(new Action0() { + Completable c = error.completable.doAfterTerminate(new Action0() { @Override public void call() { doneAfter.set(true); @@ -1940,12 +1940,12 @@ public void call() { // expected } - Assert.assertFalse("FinallyDo called", doneAfter.get()); + Assert.assertFalse("Closure called", doneAfter.get()); } @Test(expected = NullPointerException.class) - public void finallyDoNull() { - normal.completable.finallyDo(null); + public void doAfterTerminateNull() { + normal.completable.doAfterTerminate(null); } @Test(timeout = 1000) From fc38be1d149ad91dfff3d198b1815de69861e80b Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 9 Dec 2015 12:29:58 +0100 Subject: [PATCH 465/857] 1.x: fix renamed operator in Single. There was a cross dependency between two PRs yielding a broken compilation in main. --- src/main/java/rx/Single.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 3880da9b0c..7b116355a5 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -32,19 +32,8 @@ import rx.functions.Func7; import rx.functions.Func8; import rx.functions.Func9; -import rx.internal.operators.OnSubscribeToObservableFuture; -import rx.internal.operators.OperatorDelay; -import rx.internal.operators.OperatorDoOnEach; -import rx.internal.operators.OperatorDoOnUnsubscribe; -import rx.internal.operators.OperatorFinally; -import rx.internal.operators.OperatorMap; -import rx.internal.operators.OperatorObserveOn; -import rx.internal.operators.OperatorOnErrorReturn; -import rx.internal.operators.OperatorSubscribeOn; -import rx.internal.operators.OperatorTimeout; -import rx.internal.operators.OperatorZip; - import rx.annotations.Beta; +import rx.internal.operators.*; import rx.internal.producers.SingleDelayedProducer; import rx.singles.BlockingSingle; import rx.observers.SafeSubscriber; @@ -2040,6 +2029,6 @@ public final Single doOnUnsubscribe(final Action0 action) { */ @Experimental public final Single doAfterTerminate(Action0 action) { - return lift(new OperatorFinally(action)); + return lift(new OperatorDoAfterTerminate(action)); } } From 1a99ffddd1daa3cbf0d866f45fd4fafed72a0465 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Tue, 24 Nov 2015 16:26:29 +0300 Subject: [PATCH 466/857] Add Single.zip() for Iterable of Singles --- src/main/java/rx/Single.java | 70 ++++++++++++++- .../internal/operators/SingleOperatorZip.java | 72 +++++++++++++++ src/test/java/rx/SingleTest.java | 88 +++++++++++++++++++ 3 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/main/java/rx/internal/operators/SingleOperatorZip.java diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 7b116355a5..60ef33b949 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -12,6 +12,7 @@ */ package rx; +import java.util.Collection; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -32,6 +33,7 @@ import rx.functions.Func7; import rx.functions.Func8; import rx.functions.Func9; +import rx.functions.FuncN; import rx.annotations.Beta; import rx.internal.operators.*; import rx.internal.producers.SingleDelayedProducer; @@ -1196,6 +1198,30 @@ public final static Single zip(Single return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6), asObservable(o7), asObservable(o8), asObservable(o9) }).lift(new OperatorZip(zipFunction)); } + /** + * Returns a Single that emits the result of specified combiner function applied to combination of + * items emitted, in sequence, by an Iterable of other Singles. + *

    + * {@code zip} applies this function in strict sequence. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code zip} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param singles + * an Iterable of source Singles + * @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 + * @return a Single that emits the zipped results + * @see ReactiveX operators documentation: Zip + */ + public static Single zip(Iterable> singles, FuncN zipFunction) { + return SingleOperatorZip.zip(iterableToArray(singles), zipFunction); + } + /** * Returns an Observable that emits the item emitted by the source Single, then the item emitted by the * specified Single. @@ -1264,7 +1290,7 @@ public final Observable flatMapObservable(Func1Scheduler: *
    {@code map} does not operate by default on a particular {@link Scheduler}.
    * - * + * * @param func * a function to apply to the item emitted by the Single * @return a Single that emits the item from the source Single, transformed by the specified function @@ -2031,4 +2057,46 @@ public final Single doOnUnsubscribe(final Action0 action) { public final Single doAfterTerminate(Action0 action) { return lift(new OperatorDoAfterTerminate(action)); } + + /** + * FOR INTERNAL USE ONLY. + *

    + * Converts {@link Iterable} of {@link Single} to array of {@link Single}. + * + * @param singlesIterable + * non null iterable of {@link Single}. + * @return array of {@link Single} with same length as passed iterable. + */ + @SuppressWarnings("unchecked") + static Single[] iterableToArray(final Iterable> singlesIterable) { + final Single[] singlesArray; + int count; + + if (singlesIterable instanceof Collection) { + Collection> list = (Collection>) singlesIterable; + count = list.size(); + singlesArray = list.toArray(new Single[count]); + } else { + Single[] tempArray = new Single[8]; // Magic number used just to reduce number of allocations. + count = 0; + for (Single s : singlesIterable) { + if (count == tempArray.length) { + Single[] sb = new Single[count + (count >> 2)]; + System.arraycopy(tempArray, 0, sb, 0, count); + tempArray = sb; + } + tempArray[count] = s; + count++; + } + + if (tempArray.length == count) { + singlesArray = tempArray; + } else { + singlesArray = new Single[count]; + System.arraycopy(tempArray, 0, singlesArray, 0, count); + } + } + + return singlesArray; + } } diff --git a/src/main/java/rx/internal/operators/SingleOperatorZip.java b/src/main/java/rx/internal/operators/SingleOperatorZip.java new file mode 100644 index 0000000000..936750941f --- /dev/null +++ b/src/main/java/rx/internal/operators/SingleOperatorZip.java @@ -0,0 +1,72 @@ +package rx.internal.operators; + +import rx.Single; +import rx.SingleSubscriber; +import rx.exceptions.Exceptions; +import rx.functions.FuncN; +import rx.plugins.RxJavaPlugins; +import rx.subscriptions.CompositeSubscription; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class SingleOperatorZip { + + public static Single zip(final Single[] singles, final FuncN zipper) { + return Single.create(new Single.OnSubscribe() { + @Override + public void call(final SingleSubscriber subscriber) { + final AtomicInteger wip = new AtomicInteger(singles.length); + final AtomicBoolean once = new AtomicBoolean(); + final Object[] values = new Object[singles.length]; + + CompositeSubscription compositeSubscription = new CompositeSubscription(); + subscriber.add(compositeSubscription); + + for (int i = 0; i < singles.length; i++) { + if (compositeSubscription.isUnsubscribed() || once.get()) { + break; + } + + final int j = i; + SingleSubscriber singleSubscriber = new SingleSubscriber() { + @Override + public void onSuccess(T value) { + values[j] = value; + if (wip.decrementAndGet() == 0) { + R r; + + try { + r = zipper.call(values); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + onError(e); + return; + } + + subscriber.onSuccess(r); + } + } + + @Override + public void onError(Throwable error) { + if (once.compareAndSet(false, true)) { + subscriber.onError(error); + } else { + RxJavaPlugins.getInstance().getErrorHandler().handleError(error); + } + } + }; + + compositeSubscription.add(singleSubscriber); + + if (compositeSubscription.isUnsubscribed() || once.get()) { + break; + } + + singles[i].subscribe(singleSubscriber); + } + } + }); + } +} diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 17e3367835..2871450708 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -26,6 +26,11 @@ import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -45,6 +50,7 @@ import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; +import rx.functions.FuncN; import rx.schedulers.TestScheduler; import rx.singles.BlockingSingle; import rx.observers.TestSubscriber; @@ -113,6 +119,57 @@ public String call(String a, String b) { ts.assertReceivedOnNext(Arrays.asList("AB")); } + @Test + public void zipIterableShouldZipListOfSingles() { + TestSubscriber ts = new TestSubscriber(); + Iterable> singles = Arrays.asList(Single.just(1), Single.just(2), Single.just(3)); + + Single + .zip(singles, new FuncN() { + @Override + public String call(Object... args) { + StringBuilder stringBuilder = new StringBuilder(); + for (Object arg : args) { + stringBuilder.append(arg); + } + return stringBuilder.toString(); + } + }).subscribe(ts); + + ts.assertValue("123"); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void zipIterableShouldZipSetOfSingles() { + TestSubscriber ts = new TestSubscriber(); + Set> singlesSet = Collections.newSetFromMap(new LinkedHashMap, Boolean>(2)); + Single s1 = Single.just("1"); + Single s2 = Single.just("2"); + Single s3 = Single.just("3"); + + singlesSet.add(s1); + singlesSet.add(s2); + singlesSet.add(s3); + + Single + .zip(singlesSet, new FuncN() { + @Override + public String call(Object... args) { + StringBuilder stringBuilder = new StringBuilder(); + for (Object arg : args) { + stringBuilder.append(arg); + } + return stringBuilder.toString(); + } + }).subscribe(ts); + + ts.assertValue("123"); + ts.assertNoErrors(); + ts.assertCompleted(); + } + @Test public void testZipWith() { TestSubscriber ts = new TestSubscriber(); @@ -941,4 +998,35 @@ public void doAfterTerminateActionShouldNotBeInvokedUntilSubscriberSubscribes() verifyZeroInteractions(action); } + + @Test(expected = NullPointerException.class) + public void iterableToArrayShouldThrowNullPointerExceptionIfIterableNull() { + Single.iterableToArray(null); + } + + @Test + public void iterableToArrayShouldConvertList() { + List> singlesList = Arrays.asList(Single.just("1"), Single.just("2")); + + Single[] singlesArray = Single.iterableToArray(singlesList); + assertEquals(2, singlesArray.length); + assertSame(singlesList.get(0), singlesArray[0]); + assertSame(singlesList.get(1), singlesArray[1]); + } + + @Test + public void iterableToArrayShouldConvertSet() { + // Just to trigger different path of the code that handles non-list iterables. + Set> singlesSet = Collections.newSetFromMap(new LinkedHashMap, Boolean>(2)); + Single s1 = Single.just("1"); + Single s2 = Single.just("2"); + + singlesSet.add(s1); + singlesSet.add(s2); + + Single[] singlesArray = Single.iterableToArray(singlesSet); + assertEquals(2, singlesArray.length); + assertSame(s1, singlesArray[0]); + assertSame(s2, singlesArray[1]); + } } From f8e5edf1da342b174c18bd1876b3702037959b0d Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Tue, 8 Dec 2015 19:16:10 -0800 Subject: [PATCH 467/857] Implemented Observable#toCompletable --- src/main/java/rx/Completable.java | 2 +- src/main/java/rx/Observable.java | 25 +++++ .../operators/OnSubscribeCompletableTest.java | 98 +++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/test/java/rx/internal/operators/OnSubscribeCompletableTest.java diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index 5fd7216a3b..db4c5bde97 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -568,7 +568,7 @@ public void onNext(Object t) { } }; cs.onSubscribe(subscriber); - flowable.subscribe(subscriber); + flowable.unsafeSubscribe(subscriber); } }); } diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 04551d98af..bd8facd575 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -232,6 +232,31 @@ public Single toSingle() { return new Single(OnSubscribeSingle.create(this)); } + /** + * Returns a Completable that discards all onNext emissions (similar to + * {@code ignoreAllElements()}) and calls onCompleted when this source observable calls + * onCompleted. Error terminal events are propagated. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code toCompletable} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @return a Completable that calls onCompleted on it's subscriber when the source Observable + * calls onCompleted + * @see ReactiveX documentation: + * Completable + * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical + * with the release number) + */ + @Experimental + public Completable toCompletable() { + return Completable.fromObservable(this); + } + /* ********************************************************************************************************* * Operators Below Here diff --git a/src/test/java/rx/internal/operators/OnSubscribeCompletableTest.java b/src/test/java/rx/internal/operators/OnSubscribeCompletableTest.java new file mode 100644 index 0000000000..e30bb78062 --- /dev/null +++ b/src/test/java/rx/internal/operators/OnSubscribeCompletableTest.java @@ -0,0 +1,98 @@ +/** + * 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.assertFalse; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import rx.Completable; +import rx.Observable; +import rx.functions.Action0; +import rx.observers.TestSubscriber; + +public class OnSubscribeCompletableTest { + + @Test + public void testJustSingleItemObservable() { + TestSubscriber subscriber = TestSubscriber.create(); + Completable cmp = Observable.just("Hello World!").toCompletable(); + cmp.subscribe(subscriber); + + subscriber.assertNoValues(); + subscriber.assertCompleted(); + subscriber.assertNoErrors(); + } + + @Test + public void testErrorObservable() { + TestSubscriber subscriber = TestSubscriber.create(); + IllegalArgumentException error = new IllegalArgumentException("Error"); + Completable cmp = Observable.error(error).toCompletable(); + cmp.subscribe(subscriber); + + subscriber.assertError(error); + subscriber.assertNoValues(); + } + + @Test + public void testJustTwoEmissionsObservableThrowsError() { + TestSubscriber subscriber = TestSubscriber.create(); + Completable cmp = Observable.just("First", "Second").toCompletable(); + cmp.subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertNoValues(); + } + + @Test + public void testEmptyObservable() { + TestSubscriber subscriber = TestSubscriber.create(); + Completable cmp = Observable.empty().toCompletable(); + cmp.subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertNoValues(); + subscriber.assertCompleted(); + } + + @Test + public void testNeverObservable() { + TestSubscriber subscriber = TestSubscriber.create(); + Completable cmp = Observable.never().toCompletable(); + cmp.subscribe(subscriber); + + subscriber.assertNoTerminalEvent(); + subscriber.assertNoValues(); + } + + @Test + public void testShouldUseUnsafeSubscribeInternallyNotSubscribe() { + TestSubscriber subscriber = TestSubscriber.create(); + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Completable cmp = Observable.just("Hello World!").doOnUnsubscribe(new Action0() { + + @Override + public void call() { + unsubscribed.set(true); + }}).toCompletable(); + cmp.subscribe(subscriber); + subscriber.assertCompleted(); + assertFalse(unsubscribed.get()); + } +} From bfe74125e90ae24e90c4e2cd7e5576cfbad49823 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 6 Nov 2015 09:03:34 -0500 Subject: [PATCH 468/857] Rename cache(int) to cacheWithCapacityHint(int) The parameter is a capacity hint, but more frequently confused with a buffer size like replay(int) than it is correctly understood. It also offers no guarantees, only the weak hope of optimization. This change renames the method, deprecating the old name. It also adds javadoc calling out that the parameter is not a bound and referencing replay(int).autoConnect() as a way to achieve that behavior. --- src/main/java/rx/Observable.java | 18 +++++++++++++++--- src/test/java/rx/ObservableTests.java | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index cb701d936d..71c8635bb0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3664,6 +3664,15 @@ public final Observable cache() { return CachedObservable.from(this); } + /** + * @see #cacheWithInitialCapacity(int) + * @deprecated Use {@link #cacheWithInitialCapacity(int)} instead. + */ + @Deprecated + public final Observable cache(int initialCapacity) { + return cacheWithInitialCapacity(initialCapacity); + } + /** * Caches emissions from the source Observable and replays them in order to any subsequent Subscribers. * This method has similar behavior to {@link #replay} except that this auto-subscribes to the source @@ -3689,14 +3698,17 @@ public final Observable cache() { *
    Scheduler:
    *
    {@code cache} does not operate by default on a particular {@link Scheduler}.
    * + *

    + * Note: The capacity hint is not an upper bound on cache size. For that, consider + * {@link #replay(int)} in combination with {@link ConnectableObservable#autoConnect()} or similar. * - * @param capacityHint hint for number of items to cache (for optimizing underlying data structure) + * @param initialCapacity hint for number of items to cache (for optimizing underlying data structure) * @return an Observable that, when first subscribed to, caches all of its items and notifications for the * benefit of subsequent subscribers * @see ReactiveX operators documentation: Replay */ - public final Observable cache(int capacityHint) { - return CachedObservable.from(this, capacityHint); + public final Observable cacheWithInitialCapacity(int initialCapacity) { + return CachedObservable.from(this, initialCapacity); } /** diff --git a/src/test/java/rx/ObservableTests.java b/src/test/java/rx/ObservableTests.java index d59e8c41a9..2d6598132b 100644 --- a/src/test/java/rx/ObservableTests.java +++ b/src/test/java/rx/ObservableTests.java @@ -663,7 +663,7 @@ public void run() { } }).start(); } - }).cache(1); + }).cacheWithInitialCapacity(1); // we then expect the following 2 subscriptions to get that same value final CountDownLatch latch = new CountDownLatch(2); From 3992b99f37329ba168782efb90070c3b019e48d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 14 Dec 2015 23:59:00 +0100 Subject: [PATCH 469/857] 1.x: compensate for drastic clock drifts when scheduling periodic tasks --- src/main/java/rx/Scheduler.java | 40 +++++- src/test/java/rx/SchedulerWorkerTest.java | 153 ++++++++++++++++++++++ 2 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 src/test/java/rx/SchedulerWorkerTest.java diff --git a/src/main/java/rx/Scheduler.java b/src/main/java/rx/Scheduler.java index 12922bc4a4..921528c875 100644 --- a/src/main/java/rx/Scheduler.java +++ b/src/main/java/rx/Scheduler.java @@ -43,6 +43,17 @@ public abstract class Scheduler { * maintenance. */ + /** + * The tolerance for a clock drift in nanoseconds where the periodic scheduler will rebase. + *

    + * The associated system parameter, {@code rx.scheduler.drift-tolerance}, expects its value in minutes. + */ + static final long CLOCK_DRIFT_TOLERANCE_NANOS; + static { + CLOCK_DRIFT_TOLERANCE_NANOS = TimeUnit.MINUTES.toNanos( + Long.getLong("rx.scheduler.drift-tolerance", 15)); + } + /** * Retrieves or creates a new {@link Scheduler.Worker} that represents serial execution of actions. *

    @@ -109,17 +120,38 @@ public abstract static class Worker implements Subscription { */ public Subscription schedulePeriodically(final Action0 action, long initialDelay, long period, TimeUnit unit) { final long periodInNanos = unit.toNanos(period); - final long startInNanos = TimeUnit.MILLISECONDS.toNanos(now()) + unit.toNanos(initialDelay); + final long firstNowNanos = TimeUnit.MILLISECONDS.toNanos(now()); + final long firstStartInNanos = firstNowNanos + unit.toNanos(initialDelay); final MultipleAssignmentSubscription mas = new MultipleAssignmentSubscription(); final Action0 recursiveAction = new Action0() { - long count = 0; + long count; + long lastNowNanos = firstNowNanos; + long startInNanos = firstStartInNanos; @Override public void call() { if (!mas.isUnsubscribed()) { action.call(); - long nextTick = startInNanos + (++count * periodInNanos); - mas.set(schedule(this, nextTick - TimeUnit.MILLISECONDS.toNanos(now()), TimeUnit.NANOSECONDS)); + + long nextTick; + + long nowNanos = TimeUnit.MILLISECONDS.toNanos(now()); + // If the clock moved in a direction quite a bit, rebase the repetition period + if (nowNanos + CLOCK_DRIFT_TOLERANCE_NANOS < lastNowNanos + || nowNanos >= lastNowNanos + periodInNanos + CLOCK_DRIFT_TOLERANCE_NANOS) { + nextTick = nowNanos + periodInNanos; + /* + * Shift the start point back by the drift as if the whole thing + * started count periods ago. + */ + startInNanos = nextTick - (periodInNanos * (++count)); + } else { + nextTick = startInNanos + (++count * periodInNanos); + } + lastNowNanos = nowNanos; + + long delay = nextTick - nowNanos; + mas.set(schedule(this, delay, TimeUnit.NANOSECONDS)); } } }; diff --git a/src/test/java/rx/SchedulerWorkerTest.java b/src/test/java/rx/SchedulerWorkerTest.java new file mode 100644 index 0000000000..8bb1094b46 --- /dev/null +++ b/src/test/java/rx/SchedulerWorkerTest.java @@ -0,0 +1,153 @@ +/** + * 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; + +import static org.junit.Assert.assertTrue; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import rx.functions.Action0; +import rx.schedulers.Schedulers; + +public class SchedulerWorkerTest { + + static final class CustomDriftScheduler extends Scheduler { + public volatile long drift; + @Override + public Worker createWorker() { + final Worker w = Schedulers.computation().createWorker(); + return new Worker() { + + @Override + public void unsubscribe() { + w.unsubscribe(); + } + + @Override + public boolean isUnsubscribed() { + return w.isUnsubscribed(); + } + + @Override + public Subscription schedule(Action0 action) { + return w.schedule(action); + } + + @Override + public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { + return w.schedule(action, delayTime, unit); + } + + @Override + public long now() { + return super.now() + drift; + } + }; + } + + @Override + public long now() { + return super.now() + drift; + } + } + + @Test + public void testCurrentTimeDriftBackwards() throws Exception { + CustomDriftScheduler s = new CustomDriftScheduler(); + + Scheduler.Worker w = s.createWorker(); + + try { + final List times = new ArrayList(); + + Subscription d = w.schedulePeriodically(new Action0() { + @Override + public void call() { + times.add(System.currentTimeMillis()); + } + }, 100, 100, TimeUnit.MILLISECONDS); + + Thread.sleep(150); + + s.drift = -1000 - TimeUnit.NANOSECONDS.toMillis(Scheduler.CLOCK_DRIFT_TOLERANCE_NANOS); + + Thread.sleep(400); + + d.unsubscribe(); + + Thread.sleep(150); + + System.out.println("Runs: " + times.size()); + + for (int i = 0; i < times.size() - 1 ; i++) { + long diff = times.get(i + 1) - times.get(i); + System.out.println("Diff #" + i + ": " + diff); + assertTrue("" + i + ":" + diff, diff < 150 && diff > 50); + } + + assertTrue("Too few invocations: " + times.size(), times.size() > 2); + + } finally { + w.unsubscribe(); + } + + } + + @Test + public void testCurrentTimeDriftForwards() throws Exception { + CustomDriftScheduler s = new CustomDriftScheduler(); + + Scheduler.Worker w = s.createWorker(); + + try { + final List times = new ArrayList(); + + Subscription d = w.schedulePeriodically(new Action0() { + @Override + public void call() { + times.add(System.currentTimeMillis()); + } + }, 100, 100, TimeUnit.MILLISECONDS); + + Thread.sleep(150); + + s.drift = 1000 + TimeUnit.NANOSECONDS.toMillis(Scheduler.CLOCK_DRIFT_TOLERANCE_NANOS); + + Thread.sleep(400); + + d.unsubscribe(); + + Thread.sleep(150); + + System.out.println("Runs: " + times.size()); + + assertTrue(times.size() > 2); + + for (int i = 0; i < times.size() - 1 ; i++) { + long diff = times.get(i + 1) - times.get(i); + System.out.println("Diff #" + i + ": " + diff); + assertTrue("Diff out of range: " + diff, diff < 250 && diff > 50); + } + + } finally { + w.unsubscribe(); + } + + } +} From 375f8d5652608241437c31cfe486749b8473bd52 Mon Sep 17 00:00:00 2001 From: ItsPriyesh Date: Tue, 15 Dec 2015 00:24:26 -0800 Subject: [PATCH 470/857] Fix typo in CompositeException documentation --- src/main/java/rx/exceptions/CompositeException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/exceptions/CompositeException.java b/src/main/java/rx/exceptions/CompositeException.java index 7d6e37e8b9..79a49a7e74 100644 --- a/src/main/java/rx/exceptions/CompositeException.java +++ b/src/main/java/rx/exceptions/CompositeException.java @@ -28,7 +28,7 @@ /** * Represents an exception that is a composite of one or more other exceptions. A {@code CompositeException} * does not modify the structure of any exception it wraps, but at print-time it iterates through the list of - * Throwables contained in the composit in order to print them all. + * Throwables contained in the composite in order to print them all. * * Its invariant is to contain an immutable, ordered (by insertion order), unique list of non-composite * exceptions. You can retrieve individual exceptions in this list with {@link #getExceptions()}. From efec486d36c3c2548f07cde8c738d38b55603c49 Mon Sep 17 00:00:00 2001 From: Achintha Gunasekara Date: Fri, 18 Dec 2015 11:40:22 +1100 Subject: [PATCH 471/857] Update ReplaySubjectPerf.java --- src/perf/java/rx/subjects/ReplaySubjectPerf.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/perf/java/rx/subjects/ReplaySubjectPerf.java b/src/perf/java/rx/subjects/ReplaySubjectPerf.java index 821359e083..5d463efb53 100644 --- a/src/perf/java/rx/subjects/ReplaySubjectPerf.java +++ b/src/perf/java/rx/subjects/ReplaySubjectPerf.java @@ -70,9 +70,11 @@ public void onNext(Object o) { sum.incrementAndGet(); } }); + for (int i = 0; i < input.nextRuns; i++) { subject.onNext("Response"); } + subject.onCompleted(); latch.await(); bh.consume(sum); @@ -95,6 +97,7 @@ private void subscribeAfterEvents(ReplaySubject subject, final Input inp for (int i = 0; i < input.nextRuns; i++) { subject.onNext("Response"); } + subject.onCompleted(); subject.subscribe(new Observer() { From da99a5ef57853b0f65895b09e680212b7a33b60d Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Mon, 21 Dec 2015 16:56:44 +1100 Subject: [PATCH 472/857] add more detail to groupBy javadoc --- src/main/java/rx/Observable.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c907d73280..8a68e2050f 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5577,14 +5577,17 @@ public final void forEach(final Action1 onNext, final Action1 * *

    * Note: A {@link GroupedObservable} will cache the items it is to emit until such time as it * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may - * discard their buffers by applying an operator like {@link #take}{@code (0)} to them. + * discard their buffers by applying an operator like {@link #ignoreElements} to them. *

    *
    Scheduler:
    *
    {@code groupBy} does not operate by default on a particular {@link Scheduler}.
    @@ -5609,14 +5612,17 @@ public final Observable> groupBy(final Func1 * *

    * Note: A {@link GroupedObservable} will cache the items it is to emit until such time as it * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may - * discard their buffers by applying an operator like {@link #take}{@code (0)} to them. + * discard their buffers by applying an operator like {@link #ignoreElements} to them. *

    *
    Scheduler:
    *
    {@code groupBy} does not operate by default on a particular {@link Scheduler}.
    From d1af6be08bbeee65caf9c5d4264d9c2c7dc94bf5 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 21 Dec 2015 19:39:11 +0100 Subject: [PATCH 473/857] 1.x: fix Completable.using not disposing the resource if the factory crashes during the subscription phase. This PR fixes the cases when the Completable factory throws an exception or returns null and the resource is not disposed before reporting error to the subscriber. --- src/main/java/rx/Completable.java | 25 ++++- src/test/java/rx/CompletableTest.java | 134 ++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index aa222b9e8e..fb864ff14c 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -16,13 +16,13 @@ package rx; -import java.util.Iterator; +import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import rx.Observable.OnSubscribe; import rx.annotations.Experimental; -import rx.exceptions.Exceptions; +import rx.exceptions.*; import rx.functions.*; import rx.internal.operators.*; import rx.internal.util.*; @@ -864,12 +864,33 @@ public void call(final CompletableSubscriber s) { try { cs = completableFunc1.call(resource); } catch (Throwable e) { + try { + disposer.call(resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(e); + Exceptions.throwIfFatal(ex); + + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(new CompositeException(Arrays.asList(e, ex))); + return; + } + Exceptions.throwIfFatal(e); + s.onSubscribe(Subscriptions.unsubscribed()); s.onError(e); return; } if (cs == null) { + try { + disposer.call(resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + s.onSubscribe(Subscriptions.unsubscribed()); + s.onError(new CompositeException(Arrays.asList(new NullPointerException("The completable supplied is null"), ex))); + return; + } s.onSubscribe(Subscriptions.unsubscribed()); s.onError(new NullPointerException("The completable supplied is null")); return; diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index 09d71a9ff7..7c9b2fe70b 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -31,6 +31,9 @@ import rx.subjects.PublishSubject; import rx.subscriptions.*; +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + /** * Test Completable methods and operators. */ @@ -3410,4 +3413,135 @@ public void endWithFlowableError() { ts.assertError(TestException.class); ts.assertNotCompleted(); } + + @Test + public void usingFactoryThrows() { + @SuppressWarnings("unchecked") + Action1 onDispose = mock(Action1.class); + + TestSubscriber ts = TestSubscriber.create(); + + Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, + new Func1() { + @Override + public Completable call(Integer t) { + throw new TestException(); + } + }, onDispose).subscribe(ts); + + verify(onDispose).call(1); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(TestException.class); + } + + @Test + public void usingFactoryAndDisposerThrow() { + Action1 onDispose = new Action1() { + @Override + public void call(Integer t) { + throw new TestException(); + } + }; + + TestSubscriber ts = TestSubscriber.create(); + + Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, + new Func1() { + @Override + public Completable call(Integer t) { + throw new TestException(); + } + }, onDispose).subscribe(ts); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(CompositeException.class); + + CompositeException ex = (CompositeException)ts.getOnErrorEvents().get(0); + + List listEx = ex.getExceptions(); + + assertEquals(2, listEx.size()); + + assertTrue(listEx.get(0).toString(), listEx.get(0) instanceof TestException); + assertTrue(listEx.get(1).toString(), listEx.get(1) instanceof TestException); + } + + @Test + public void usingFactoryReturnsNull() { + @SuppressWarnings("unchecked") + Action1 onDispose = mock(Action1.class); + + TestSubscriber ts = TestSubscriber.create(); + + Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, + new Func1() { + @Override + public Completable call(Integer t) { + return null; + } + }, onDispose).subscribe(ts); + + verify(onDispose).call(1); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(NullPointerException.class); + } + + @Test + public void usingFactoryReturnsNullAndDisposerThrows() { + Action1 onDispose = new Action1() { + @Override + public void call(Integer t) { + throw new TestException(); + } + }; + + TestSubscriber ts = TestSubscriber.create(); + + Completable.using(new Func0() { + @Override + public Integer call() { + return 1; + } + }, + new Func1() { + @Override + public Completable call(Integer t) { + return null; + } + }, onDispose).subscribe(ts); + + ts.assertNoValues(); + ts.assertNotCompleted(); + ts.assertError(CompositeException.class); + + CompositeException ex = (CompositeException)ts.getOnErrorEvents().get(0); + + List listEx = ex.getExceptions(); + + assertEquals(2, listEx.size()); + + assertTrue(listEx.get(0).toString(), listEx.get(0) instanceof NullPointerException); + assertTrue(listEx.get(1).toString(), listEx.get(1) instanceof TestException); + } + } \ No newline at end of file From 56cd393394034c8a81ae44ec23cf170c49e16022 Mon Sep 17 00:00:00 2001 From: "mariusz.luciow" Date: Thu, 24 Dec 2015 11:39:50 +0100 Subject: [PATCH 474/857] Fixed typo --- src/main/java/rx/Observable.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c907d73280..327903e1f0 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3786,7 +3786,7 @@ public final R call(R state, T value) { /** * Returns a new Observable that emits items resulting from applying a function that you supply to each item * emitted by the source Observable, where that function returns an Observable, and then emitting the items - * that result from concatinating those resulting Observables. + * that result from concatenating those resulting Observables. *

    * *

    @@ -3798,7 +3798,7 @@ public final R call(R state, T value) { * a function that, when applied to an item emitted by the source Observable, returns an * Observable * @return an Observable that emits the result of applying the transformation function to each item emitted - * by the source Observable and concatinating the Observables obtained from this transformation + * by the source Observable and concatenating the Observables obtained from this transformation * @see ReactiveX operators documentation: FlatMap */ public final Observable concatMap(Func1> func) { From 04be9ee6cd6748a16d28335d46cb3b9c435b4fff Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Fri, 25 Dec 2015 03:28:43 -0500 Subject: [PATCH 475/857] Remove the need for javac to generate synthetic methods. Outer classes accessing inner class private fields and methods (and vise versa) causes javac to generate package-scoped trampolines. These bloat the class files and for Android create needless method that eat away at our fixed limit of methods in an application. By simply promoting the private interactions to package scope directly, the synthetic methods do not need generated. --- src/main/java/rx/Observable.java | 2 +- src/main/java/rx/Single.java | 2 +- src/main/java/rx/functions/Actions.java | 3 +++ .../operators/BlockingOperatorMostRecent.java | 4 ++-- .../operators/BlockingOperatorNext.java | 5 ++++- .../operators/BufferUntilSubscriber.java | 2 +- .../internal/operators/NotificationLite.java | 2 +- .../rx/internal/operators/OnSubscribeAmb.java | 4 ++-- .../operators/OnSubscribeFromIterable.java | 2 +- .../internal/operators/OnSubscribeRange.java | 2 +- .../internal/operators/OnSubscribeRedo.java | 10 +++++----- .../operators/OnSubscribeRefCount.java | 6 +++--- .../OnSubscribeToObservableFuture.java | 2 +- .../internal/operators/OnSubscribeUsing.java | 2 +- .../rx/internal/operators/OperatorAll.java | 2 +- .../rx/internal/operators/OperatorAny.java | 4 ++-- .../operators/OperatorAsObservable.java | 2 +- .../rx/internal/operators/OperatorCast.java | 2 +- .../rx/internal/operators/OperatorConcat.java | 2 +- .../operators/OperatorDematerialize.java | 2 +- .../internal/operators/OperatorDoOnEach.java | 2 +- .../operators/OperatorDoOnRequest.java | 4 ++-- .../internal/operators/OperatorElementAt.java | 6 +++--- .../rx/internal/operators/OperatorFilter.java | 2 +- .../operators/OperatorIgnoreElements.java | 2 +- .../rx/internal/operators/OperatorMap.java | 2 +- .../operators/OperatorMapNotification.java | 10 +++++----- .../operators/OperatorMaterialize.java | 2 +- .../rx/internal/operators/OperatorMerge.java | 2 +- .../internal/operators/OperatorMulticast.java | 4 ++-- .../OperatorOnBackpressureBuffer.java | 4 ++-- .../operators/OperatorOnBackpressureDrop.java | 4 ++-- .../OperatorOnBackpressureLatest.java | 2 +- .../OperatorOnErrorResumeNextViaFunction.java | 2 +- .../rx/internal/operators/OperatorScan.java | 2 +- .../operators/OperatorSequenceEqual.java | 2 +- .../internal/operators/OperatorSerialize.java | 2 +- .../rx/internal/operators/OperatorSingle.java | 4 ++-- .../internal/operators/OperatorSkipLast.java | 2 +- .../operators/OperatorSkipLastTimed.java | 4 ++-- .../internal/operators/OperatorSkipWhile.java | 2 +- .../rx/internal/operators/OperatorSwitch.java | 6 +++--- .../internal/operators/OperatorTakeLast.java | 2 +- .../operators/OperatorTakeLastOne.java | 2 +- .../operators/OperatorTakeLastTimed.java | 6 +++--- .../operators/OperatorTakeUntilPredicate.java | 4 ++-- .../internal/operators/OperatorTakeWhile.java | 2 +- .../operators/OperatorThrottleFirst.java | 4 ++-- .../operators/OperatorTimeInterval.java | 2 +- .../operators/OperatorTimeoutBase.java | 4 ++-- .../internal/operators/OperatorTimestamp.java | 2 +- .../rx/internal/operators/OperatorToMap.java | 4 ++-- .../operators/OperatorToMultimap.java | 6 +++--- .../operators/OperatorToObservableList.java | 2 +- .../OperatorToObservableSortedList.java | 6 ++++-- .../operators/OperatorUnsubscribeOn.java | 2 +- .../rx/internal/operators/OperatorZip.java | 2 +- .../schedulers/EventLoopsScheduler.java | 8 ++++---- .../internal/schedulers/ScheduledAction.java | 2 +- .../rx/internal/util/IndexedRingBuffer.java | 12 ++++++++--- .../java/rx/internal/util/ObjectPool.java | 6 +++--- .../util/ScalarSynchronousObservable.java | 4 ++-- .../rx/internal/util/UtilityFunctions.java | 3 +++ .../java/rx/observables/AsyncOnSubscribe.java | 6 +++--- .../rx/observables/BlockingObservable.java | 10 +++++----- .../java/rx/observables/SyncOnSubscribe.java | 6 +++--- .../rx/schedulers/CachedThreadScheduler.java | 4 ++-- .../rx/schedulers/ImmediateScheduler.java | 3 +++ .../java/rx/schedulers/TestScheduler.java | 20 ++++++++++++------- .../rx/schedulers/TrampolineScheduler.java | 9 ++++++--- src/main/java/rx/subjects/TestSubject.java | 6 +++--- .../java/rx/subscriptions/Subscriptions.java | 2 +- 72 files changed, 157 insertions(+), 128 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index c907d73280..fecad14ae4 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -60,7 +60,7 @@ protected Observable(OnSubscribe f) { this.onSubscribe = f; } - private static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); + static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); /** * Returns an Observable that will execute the specified function when a {@link Subscriber} subscribes to diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 60ef33b949..2d332e5de8 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -111,7 +111,7 @@ private Single(final Observable.OnSubscribe f) { this.onSubscribe = f; } - private static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); + static final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); /** * Returns a Single that will execute the specified function when a {@link SingleSubscriber} executes it or diff --git a/src/main/java/rx/functions/Actions.java b/src/main/java/rx/functions/Actions.java index 862bba221c..ea18eaed91 100644 --- a/src/main/java/rx/functions/Actions.java +++ b/src/main/java/rx/functions/Actions.java @@ -43,6 +43,9 @@ private static final class EmptyAction imple Action8, Action9, ActionN { + EmptyAction() { + } + @Override public void call() { } diff --git a/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java b/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java index 89d236aabb..9b6cf64bf2 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorMostRecent.java @@ -63,8 +63,8 @@ public Iterator iterator() { private static final class MostRecentObserver extends Subscriber { final NotificationLite nl = NotificationLite.instance(); volatile Object value; - - private MostRecentObserver(T value) { + + MostRecentObserver(T value) { this.value = nl.next(value); } diff --git a/src/main/java/rx/internal/operators/BlockingOperatorNext.java b/src/main/java/rx/internal/operators/BlockingOperatorNext.java index f81577f9b3..7a55a663eb 100644 --- a/src/main/java/rx/internal/operators/BlockingOperatorNext.java +++ b/src/main/java/rx/internal/operators/BlockingOperatorNext.java @@ -66,7 +66,7 @@ public Iterator iterator() { private Throwable error = null; private boolean started = false; - private NextIterator(Observable items, NextObserver observer) { + NextIterator(Observable items, NextObserver observer) { this.items = items; this.observer = observer; } @@ -149,6 +149,9 @@ private static class NextObserver extends Subscriber> buf = new ArrayBlockingQueue>(1); final AtomicInteger waiting = new AtomicInteger(); + NextObserver() { + } + @Override public void onCompleted() { // ignore diff --git a/src/main/java/rx/internal/operators/BufferUntilSubscriber.java b/src/main/java/rx/internal/operators/BufferUntilSubscriber.java index f486c397f7..5e794de018 100644 --- a/src/main/java/rx/internal/operators/BufferUntilSubscriber.java +++ b/src/main/java/rx/internal/operators/BufferUntilSubscriber.java @@ -187,7 +187,7 @@ public boolean hasObservers() { } @SuppressWarnings("rawtypes") - private final static Observer EMPTY_OBSERVER = new Observer() { + final static Observer EMPTY_OBSERVER = new Observer() { @Override public void onCompleted() { diff --git a/src/main/java/rx/internal/operators/NotificationLite.java b/src/main/java/rx/internal/operators/NotificationLite.java index cd223e6784..0293beab95 100644 --- a/src/main/java/rx/internal/operators/NotificationLite.java +++ b/src/main/java/rx/internal/operators/NotificationLite.java @@ -71,7 +71,7 @@ public String toString() { private static class OnErrorSentinel implements Serializable { private static final long serialVersionUID = 3; - private final Throwable e; + final Throwable e; public OnErrorSentinel(Throwable e) { this.e = e; diff --git a/src/main/java/rx/internal/operators/OnSubscribeAmb.java b/src/main/java/rx/internal/operators/OnSubscribeAmb.java index 2ddd0dc820..2fe48d812f 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeAmb.java +++ b/src/main/java/rx/internal/operators/OnSubscribeAmb.java @@ -271,7 +271,7 @@ private static final class AmbSubscriber extends Subscriber { private final Selection selection; private boolean chosen; - private AmbSubscriber(long requested, Subscriber subscriber, Selection selection) { + AmbSubscriber(long requested, Subscriber subscriber, Selection selection) { this.subscriber = subscriber; this.selection = selection; // initial request @@ -434,7 +434,7 @@ public void request(long n) { }); } - private static void unsubscribeAmbSubscribers(Collection> ambSubscribers) { + static void unsubscribeAmbSubscribers(Collection> ambSubscribers) { if(!ambSubscribers.isEmpty()) { for (AmbSubscriber other : ambSubscribers) { other.unsubscribe(); diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java index f4790e75bd..b94e35c35c 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromIterable.java @@ -55,7 +55,7 @@ private static final class IterableProducer extends AtomicLong implements Pro private final Subscriber o; private final Iterator it; - private IterableProducer(Subscriber o, Iterator it) { + IterableProducer(Subscriber o, Iterator it) { this.o = o; this.it = it; } diff --git a/src/main/java/rx/internal/operators/OnSubscribeRange.java b/src/main/java/rx/internal/operators/OnSubscribeRange.java index 383d17f28f..c7631b2cb9 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRange.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRange.java @@ -46,7 +46,7 @@ private static final class RangeProducer extends AtomicLong implements Producer private final int end; private long index; - private RangeProducer(Subscriber o, int start, int end) { + RangeProducer(Subscriber o, int start, int end) { this.o = o; this.index = start; this.end = end; diff --git a/src/main/java/rx/internal/operators/OnSubscribeRedo.java b/src/main/java/rx/internal/operators/OnSubscribeRedo.java index 6420e66451..d30ddc1343 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRedo.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRedo.java @@ -67,7 +67,7 @@ public Notification call(Notification terminal) { }; public static final class RedoFinite implements Func1>, Observable> { - private final long count; + final long count; public RedoFinite(long count) { this.count = count; @@ -98,7 +98,7 @@ public Notification call(Notification terminalNotification) { } public static final class RetryWithPredicate implements Func1>, Observable>> { - private final Func2 predicate; + final Func2 predicate; public RetryWithPredicate(Func2 predicate) { this.predicate = predicate; @@ -173,10 +173,10 @@ public static Observable redo(Observable source, Func1(source, notificationHandler, false, false, scheduler)); } - private final Observable source; + final Observable source; private final Func1>, ? extends Observable> controlHandlerFunction; - private final boolean stopOnComplete; - private final boolean stopOnError; + final boolean stopOnComplete; + final boolean stopOnError; private final Scheduler scheduler; private OnSubscribeRedo(Observable source, Func1>, ? extends Observable> f, boolean stopOnComplete, boolean stopOnError, diff --git a/src/main/java/rx/internal/operators/OnSubscribeRefCount.java b/src/main/java/rx/internal/operators/OnSubscribeRefCount.java index cc422453f2..82ec0e6c38 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRefCount.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRefCount.java @@ -38,13 +38,13 @@ public final class OnSubscribeRefCount implements OnSubscribe { private final ConnectableObservable source; - private volatile CompositeSubscription baseSubscription = new CompositeSubscription(); - private final AtomicInteger subscriptionCount = new AtomicInteger(0); + volatile CompositeSubscription baseSubscription = new CompositeSubscription(); + final AtomicInteger subscriptionCount = new AtomicInteger(0); /** * Use this lock for every subscription and disconnect action. */ - private final ReentrantLock lock = new ReentrantLock(); + final ReentrantLock lock = new ReentrantLock(); /** * Constructor. diff --git a/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java b/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java index 72adcf5d50..0f6e2e3f76 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java +++ b/src/main/java/rx/internal/operators/OnSubscribeToObservableFuture.java @@ -41,7 +41,7 @@ private OnSubscribeToObservableFuture() { } /* package accessible for unit tests */static class ToObservableFuture implements OnSubscribe { - private final Future that; + final Future that; private final long time; private final TimeUnit unit; diff --git a/src/main/java/rx/internal/operators/OnSubscribeUsing.java b/src/main/java/rx/internal/operators/OnSubscribeUsing.java index 4355e78221..4dd483b4cc 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeUsing.java +++ b/src/main/java/rx/internal/operators/OnSubscribeUsing.java @@ -105,7 +105,7 @@ private static final class DisposeAction extends AtomicBoolean impleme private Action1 dispose; private Resource resource; - private DisposeAction(Action1 dispose, Resource resource) { + DisposeAction(Action1 dispose, Resource resource) { this.dispose = dispose; this.resource = resource; lazySet(false); // StoreStore barrier diff --git a/src/main/java/rx/internal/operators/OperatorAll.java b/src/main/java/rx/internal/operators/OperatorAll.java index 00845c7334..91160c048e 100644 --- a/src/main/java/rx/internal/operators/OperatorAll.java +++ b/src/main/java/rx/internal/operators/OperatorAll.java @@ -28,7 +28,7 @@ * */ public final class OperatorAll implements Operator { - private final Func1 predicate; + final Func1 predicate; public OperatorAll(Func1 predicate) { this.predicate = predicate; diff --git a/src/main/java/rx/internal/operators/OperatorAny.java b/src/main/java/rx/internal/operators/OperatorAny.java index ac84ec961f..50934a6513 100644 --- a/src/main/java/rx/internal/operators/OperatorAny.java +++ b/src/main/java/rx/internal/operators/OperatorAny.java @@ -27,8 +27,8 @@ * an observable sequence satisfies a condition, otherwise false. */ public final class OperatorAny implements Operator { - private final Func1 predicate; - private final boolean returnOnEmpty; + final Func1 predicate; + final boolean returnOnEmpty; public OperatorAny(Func1 predicate, boolean returnOnEmpty) { this.predicate = predicate; diff --git a/src/main/java/rx/internal/operators/OperatorAsObservable.java b/src/main/java/rx/internal/operators/OperatorAsObservable.java index 41f2cb3ebc..8efa3e8f6f 100644 --- a/src/main/java/rx/internal/operators/OperatorAsObservable.java +++ b/src/main/java/rx/internal/operators/OperatorAsObservable.java @@ -37,7 +37,7 @@ private static final class Holder { public static OperatorAsObservable instance() { return (OperatorAsObservable)Holder.INSTANCE; } - private OperatorAsObservable() { } + OperatorAsObservable() { } @Override public Subscriber call(Subscriber s) { return s; diff --git a/src/main/java/rx/internal/operators/OperatorCast.java b/src/main/java/rx/internal/operators/OperatorCast.java index 248fcb1970..825847b5ce 100644 --- a/src/main/java/rx/internal/operators/OperatorCast.java +++ b/src/main/java/rx/internal/operators/OperatorCast.java @@ -24,7 +24,7 @@ */ public class OperatorCast implements Operator { - private final Class castClass; + final Class castClass; public OperatorCast(Class castClass) { this.castClass = castClass; diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index c3ab903658..8455cc55b3 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -50,7 +50,7 @@ private static final class Holder { public static OperatorConcat instance() { return (OperatorConcat)Holder.INSTANCE; } - private OperatorConcat() { } + OperatorConcat() { } @Override public Subscriber> call(final Subscriber child) { final SerializedSubscriber s = new SerializedSubscriber(child); diff --git a/src/main/java/rx/internal/operators/OperatorDematerialize.java b/src/main/java/rx/internal/operators/OperatorDematerialize.java index d9a154d795..5fd8d7fdfa 100644 --- a/src/main/java/rx/internal/operators/OperatorDematerialize.java +++ b/src/main/java/rx/internal/operators/OperatorDematerialize.java @@ -42,7 +42,7 @@ private static final class Holder { public static OperatorDematerialize instance() { return Holder.INSTANCE; // using raw types because the type inference is not good enough } - private OperatorDematerialize() { } + OperatorDematerialize() { } @Override public Subscriber> call(final Subscriber child) { return new Subscriber>(child) { diff --git a/src/main/java/rx/internal/operators/OperatorDoOnEach.java b/src/main/java/rx/internal/operators/OperatorDoOnEach.java index 1e3a680dac..3e274b17c4 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnEach.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnEach.java @@ -25,7 +25,7 @@ * Converts the elements of an observable sequence to the specified type. */ public class OperatorDoOnEach implements Operator { - private final Observer doOnEachObserver; + final Observer doOnEachObserver; public OperatorDoOnEach(Observer doOnEachObserver) { this.doOnEachObserver = doOnEachObserver; diff --git a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java index 2c77a584ca..d68c3497aa 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java @@ -28,7 +28,7 @@ */ public class OperatorDoOnRequest implements Operator { - private final Action1 request; + final Action1 request; public OperatorDoOnRequest(Action1 request) { this.request = request; @@ -55,7 +55,7 @@ public void request(long n) { private static final class ParentSubscriber extends Subscriber { private final Subscriber child; - private ParentSubscriber(Subscriber child) { + ParentSubscriber(Subscriber child) { this.child = child; } diff --git a/src/main/java/rx/internal/operators/OperatorElementAt.java b/src/main/java/rx/internal/operators/OperatorElementAt.java index 19a156dfa2..516e73f282 100644 --- a/src/main/java/rx/internal/operators/OperatorElementAt.java +++ b/src/main/java/rx/internal/operators/OperatorElementAt.java @@ -26,9 +26,9 @@ */ public final class OperatorElementAt implements Operator { - private final int index; - private final boolean hasDefault; - private final T defaultValue; + final int index; + final boolean hasDefault; + final T defaultValue; public OperatorElementAt(int index) { this(index, null, false); diff --git a/src/main/java/rx/internal/operators/OperatorFilter.java b/src/main/java/rx/internal/operators/OperatorFilter.java index 2dbd827a94..3704dbc4a3 100644 --- a/src/main/java/rx/internal/operators/OperatorFilter.java +++ b/src/main/java/rx/internal/operators/OperatorFilter.java @@ -27,7 +27,7 @@ */ public final class OperatorFilter implements Operator { - private final Func1 predicate; + final Func1 predicate; public OperatorFilter(Func1 predicate) { this.predicate = predicate; diff --git a/src/main/java/rx/internal/operators/OperatorIgnoreElements.java b/src/main/java/rx/internal/operators/OperatorIgnoreElements.java index 3f38d8e585..00098f85a2 100644 --- a/src/main/java/rx/internal/operators/OperatorIgnoreElements.java +++ b/src/main/java/rx/internal/operators/OperatorIgnoreElements.java @@ -29,7 +29,7 @@ public static OperatorIgnoreElements instance() { return (OperatorIgnoreElements) Holder.INSTANCE; } - private OperatorIgnoreElements() { + OperatorIgnoreElements() { } diff --git a/src/main/java/rx/internal/operators/OperatorMap.java b/src/main/java/rx/internal/operators/OperatorMap.java index 5816887479..90925c2764 100644 --- a/src/main/java/rx/internal/operators/OperatorMap.java +++ b/src/main/java/rx/internal/operators/OperatorMap.java @@ -28,7 +28,7 @@ */ public final class OperatorMap implements Operator { - private final Func1 transformer; + final Func1 transformer; public OperatorMap(Func1 transformer) { this.transformer = transformer; diff --git a/src/main/java/rx/internal/operators/OperatorMapNotification.java b/src/main/java/rx/internal/operators/OperatorMapNotification.java index a0c0994032..8abe7b828e 100644 --- a/src/main/java/rx/internal/operators/OperatorMapNotification.java +++ b/src/main/java/rx/internal/operators/OperatorMapNotification.java @@ -34,9 +34,9 @@ */ public final class OperatorMapNotification implements Operator { - private final Func1 onNext; - private final Func1 onError; - private final Func0 onCompleted; + final Func1 onNext; + final Func1 onError; + final Func0 onCompleted; public OperatorMapNotification(Func1 onNext, Func1 onError, Func0 onCompleted) { this.onNext = onNext; @@ -58,8 +58,8 @@ final class MapNotificationSubscriber extends Subscriber { private final Subscriber o; private final ProducerArbiter pa; final SingleEmitter emitter; - - private MapNotificationSubscriber(ProducerArbiter pa, Subscriber o) { + + MapNotificationSubscriber(ProducerArbiter pa, Subscriber o) { this.pa = pa; this.o = o; this.emitter = new SingleEmitter(o, pa, this); diff --git a/src/main/java/rx/internal/operators/OperatorMaterialize.java b/src/main/java/rx/internal/operators/OperatorMaterialize.java index 32b49c6c77..4a90f1f43f 100644 --- a/src/main/java/rx/internal/operators/OperatorMaterialize.java +++ b/src/main/java/rx/internal/operators/OperatorMaterialize.java @@ -47,7 +47,7 @@ public static OperatorMaterialize instance() { return (OperatorMaterialize) Holder.INSTANCE; } - private OperatorMaterialize() { + OperatorMaterialize() { } @Override diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 3fd96791a0..56a7058d26 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -90,7 +90,7 @@ public static OperatorMerge instance(boolean delayErrors, int maxConcurre final boolean delayErrors; final int maxConcurrent; - private OperatorMerge(boolean delayErrors, int maxConcurrent) { + OperatorMerge(boolean delayErrors, int maxConcurrent) { this.delayErrors = delayErrors; this.maxConcurrent = maxConcurrent; } diff --git a/src/main/java/rx/internal/operators/OperatorMulticast.java b/src/main/java/rx/internal/operators/OperatorMulticast.java index 8ec88140c2..fcdededbed 100644 --- a/src/main/java/rx/internal/operators/OperatorMulticast.java +++ b/src/main/java/rx/internal/operators/OperatorMulticast.java @@ -46,9 +46,9 @@ public final class OperatorMulticast extends ConnectableObservable { final List> waitingForConnect; /** Guarded by guard. */ - private Subscriber subscription; + Subscriber subscription; // wraps subscription above for unsubscription using guard - private Subscription guardedSubscription; + Subscription guardedSubscription; public OperatorMulticast(Observable source, final Func0> subjectFactory) { this(new Object(), new AtomicReference>(), new ArrayList>(), source, subjectFactory); diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index cb39a53ef7..4aff6fc162 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -39,8 +39,8 @@ private static class Holder { public static OperatorOnBackpressureBuffer instance() { return (OperatorOnBackpressureBuffer) Holder.INSTANCE; } - - private OperatorOnBackpressureBuffer() { + + OperatorOnBackpressureBuffer() { this.capacity = null; this.onOverflow = null; } diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java index fee6289a4b..a9a8def2d4 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java @@ -38,9 +38,9 @@ public static OperatorOnBackpressureDrop instance() { return (OperatorOnBackpressureDrop)Holder.INSTANCE; } - private final Action1 onDrop; + final Action1 onDrop; - private OperatorOnBackpressureDrop() { + OperatorOnBackpressureDrop() { this(null); } diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java index 512010515c..2bf909289e 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureLatest.java @@ -194,7 +194,7 @@ void emit() { static final class LatestSubscriber extends Subscriber { private final LatestEmitter producer; - private LatestSubscriber(LatestEmitter producer) { + LatestSubscriber(LatestEmitter producer) { this.producer = producer; } diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java index 5141a0974d..b12c10d391 100644 --- a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java +++ b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java @@ -43,7 +43,7 @@ */ public final class OperatorOnErrorResumeNextViaFunction implements Operator { - private final Func1> resumeFunction; + final Func1> resumeFunction; public OperatorOnErrorResumeNextViaFunction(Func1> f) { this.resumeFunction = f; diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index f91d9b28f2..4ce2e4ce4c 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -44,7 +44,7 @@ public final class OperatorScan implements Operator { private final Func0 initialValueFactory; - private final Func2 accumulator; + final Func2 accumulator; // sentinel if we don't receive an initial value private static final Object NO_INITIAL_VALUE = new Object(); diff --git a/src/main/java/rx/internal/operators/OperatorSequenceEqual.java b/src/main/java/rx/internal/operators/OperatorSequenceEqual.java index b03855f63a..06a30edcde 100644 --- a/src/main/java/rx/internal/operators/OperatorSequenceEqual.java +++ b/src/main/java/rx/internal/operators/OperatorSequenceEqual.java @@ -33,7 +33,7 @@ private OperatorSequenceEqual() { } /** NotificationLite doesn't work as zip uses it. */ - private static final Object LOCAL_ONCOMPLETED = new Object(); + static final Object LOCAL_ONCOMPLETED = new Object(); static Observable materializeLite(Observable source) { return concat( source.map(new Func1() { diff --git a/src/main/java/rx/internal/operators/OperatorSerialize.java b/src/main/java/rx/internal/operators/OperatorSerialize.java index 334ddef679..a8d7dd4a47 100644 --- a/src/main/java/rx/internal/operators/OperatorSerialize.java +++ b/src/main/java/rx/internal/operators/OperatorSerialize.java @@ -32,7 +32,7 @@ private static final class Holder { public static OperatorSerialize instance() { return (OperatorSerialize)Holder.INSTANCE; } - private OperatorSerialize() { } + OperatorSerialize() { } @Override public Subscriber call(final Subscriber s) { return new SerializedSubscriber(new Subscriber(s) { diff --git a/src/main/java/rx/internal/operators/OperatorSingle.java b/src/main/java/rx/internal/operators/OperatorSingle.java index 53afca58c8..252b6c4ac3 100644 --- a/src/main/java/rx/internal/operators/OperatorSingle.java +++ b/src/main/java/rx/internal/operators/OperatorSingle.java @@ -46,8 +46,8 @@ private static class Holder { public static OperatorSingle instance() { return (OperatorSingle) Holder.INSTANCE; } - - private OperatorSingle() { + + OperatorSingle() { this(false, null); } diff --git a/src/main/java/rx/internal/operators/OperatorSkipLast.java b/src/main/java/rx/internal/operators/OperatorSkipLast.java index 995e4eb777..be877a6b48 100644 --- a/src/main/java/rx/internal/operators/OperatorSkipLast.java +++ b/src/main/java/rx/internal/operators/OperatorSkipLast.java @@ -26,7 +26,7 @@ */ public class OperatorSkipLast implements Operator { - private final int count; + final int count; public OperatorSkipLast(int count) { if (count < 0) { diff --git a/src/main/java/rx/internal/operators/OperatorSkipLastTimed.java b/src/main/java/rx/internal/operators/OperatorSkipLastTimed.java index 7ea9c774b0..6bc24579ae 100644 --- a/src/main/java/rx/internal/operators/OperatorSkipLastTimed.java +++ b/src/main/java/rx/internal/operators/OperatorSkipLastTimed.java @@ -29,8 +29,8 @@ */ public class OperatorSkipLastTimed implements Operator { - private final long timeInMillis; - private final Scheduler scheduler; + final long timeInMillis; + final Scheduler scheduler; public OperatorSkipLastTimed(long time, TimeUnit unit, Scheduler scheduler) { this.timeInMillis = unit.toMillis(time); diff --git a/src/main/java/rx/internal/operators/OperatorSkipWhile.java b/src/main/java/rx/internal/operators/OperatorSkipWhile.java index 37ff4b498d..7936901a0e 100644 --- a/src/main/java/rx/internal/operators/OperatorSkipWhile.java +++ b/src/main/java/rx/internal/operators/OperatorSkipWhile.java @@ -25,7 +25,7 @@ * as soon as the condition becomes false. */ public final class OperatorSkipWhile implements Operator { - private final Func2 predicate; + final Func2 predicate; public OperatorSkipWhile(Func2 predicate) { this.predicate = predicate; diff --git a/src/main/java/rx/internal/operators/OperatorSwitch.java b/src/main/java/rx/internal/operators/OperatorSwitch.java index cbd02e1b58..5f95f38c3d 100644 --- a/src/main/java/rx/internal/operators/OperatorSwitch.java +++ b/src/main/java/rx/internal/operators/OperatorSwitch.java @@ -47,9 +47,9 @@ private static final class Holder { public static OperatorSwitch instance() { return (OperatorSwitch)Holder.INSTANCE; } - - private OperatorSwitch() { } - + + OperatorSwitch() { } + @Override public Subscriber> call(final Subscriber child) { SwitchSubscriber sws = new SwitchSubscriber(child); diff --git a/src/main/java/rx/internal/operators/OperatorTakeLast.java b/src/main/java/rx/internal/operators/OperatorTakeLast.java index 54c1a0c43a..2812c4e87c 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLast.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLast.java @@ -28,7 +28,7 @@ */ public final class OperatorTakeLast implements Operator { - private final int count; + final int count; public OperatorTakeLast(int count) { if (count < 0) { diff --git a/src/main/java/rx/internal/operators/OperatorTakeLastOne.java b/src/main/java/rx/internal/operators/OperatorTakeLastOne.java index a7998a1667..6f8bf86259 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLastOne.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLastOne.java @@ -18,7 +18,7 @@ public static OperatorTakeLastOne instance() { return (OperatorTakeLastOne) Holder.INSTANCE; } - private OperatorTakeLastOne() { + OperatorTakeLastOne() { } diff --git a/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java b/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java index 48544f505c..ec7cc12493 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java +++ b/src/main/java/rx/internal/operators/OperatorTakeLastTimed.java @@ -30,9 +30,9 @@ */ public final class OperatorTakeLastTimed implements Operator { - private final long ageMillis; - private final Scheduler scheduler; - private final int count; + final long ageMillis; + final Scheduler scheduler; + final int count; public OperatorTakeLastTimed(long time, TimeUnit unit, Scheduler scheduler) { this.ageMillis = unit.toMillis(time); diff --git a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java index c65946bab1..36ce00271b 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java +++ b/src/main/java/rx/internal/operators/OperatorTakeUntilPredicate.java @@ -31,7 +31,7 @@ private final class ParentSubscriber extends Subscriber { private final Subscriber child; private boolean done = false; - private ParentSubscriber(Subscriber child) { + ParentSubscriber(Subscriber child) { this.child = child; } @@ -73,7 +73,7 @@ void downstreamRequest(long n) { } } - private final Func1 stopPredicate; + final Func1 stopPredicate; public OperatorTakeUntilPredicate(final Func1 stopPredicate) { this.stopPredicate = stopPredicate; diff --git a/src/main/java/rx/internal/operators/OperatorTakeWhile.java b/src/main/java/rx/internal/operators/OperatorTakeWhile.java index 0c34df7b6f..e241ace057 100644 --- a/src/main/java/rx/internal/operators/OperatorTakeWhile.java +++ b/src/main/java/rx/internal/operators/OperatorTakeWhile.java @@ -28,7 +28,7 @@ */ public final class OperatorTakeWhile implements Operator { - private final Func2 predicate; + final Func2 predicate; public OperatorTakeWhile(final Func1 underlying) { this(new Func2() { diff --git a/src/main/java/rx/internal/operators/OperatorThrottleFirst.java b/src/main/java/rx/internal/operators/OperatorThrottleFirst.java index 0f14839e47..2bf960931d 100644 --- a/src/main/java/rx/internal/operators/OperatorThrottleFirst.java +++ b/src/main/java/rx/internal/operators/OperatorThrottleFirst.java @@ -25,8 +25,8 @@ */ public final class OperatorThrottleFirst implements Operator { - private final long timeInMilliseconds; - private final Scheduler scheduler; + final long timeInMilliseconds; + final Scheduler scheduler; public OperatorThrottleFirst(long windowDuration, TimeUnit unit, Scheduler scheduler) { this.timeInMilliseconds = unit.toMillis(windowDuration); diff --git a/src/main/java/rx/internal/operators/OperatorTimeInterval.java b/src/main/java/rx/internal/operators/OperatorTimeInterval.java index 4b76ea768d..e73f6fd0d3 100644 --- a/src/main/java/rx/internal/operators/OperatorTimeInterval.java +++ b/src/main/java/rx/internal/operators/OperatorTimeInterval.java @@ -25,7 +25,7 @@ */ public final class OperatorTimeInterval implements Operator, T> { - private final Scheduler scheduler; + final Scheduler scheduler; public OperatorTimeInterval(Scheduler scheduler) { this.scheduler = scheduler; diff --git a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java index 65b940640c..d4700bcb9b 100644 --- a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java +++ b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java @@ -92,8 +92,8 @@ public Subscriber call(Subscriber subscriber) { final AtomicInteger terminated = new AtomicInteger(); final AtomicLong actual = new AtomicLong(); - - private TimeoutSubscriber( + + TimeoutSubscriber( SerializedSubscriber serializedSubscriber, TimeoutStub timeoutStub, SerialSubscription serial, Observable other, diff --git a/src/main/java/rx/internal/operators/OperatorTimestamp.java b/src/main/java/rx/internal/operators/OperatorTimestamp.java index 2e24fbf169..284c0e2124 100644 --- a/src/main/java/rx/internal/operators/OperatorTimestamp.java +++ b/src/main/java/rx/internal/operators/OperatorTimestamp.java @@ -27,7 +27,7 @@ */ public final class OperatorTimestamp implements Operator, T> { - private final Scheduler scheduler; + final Scheduler scheduler; public OperatorTimestamp(Scheduler scheduler) { this.scheduler = scheduler; diff --git a/src/main/java/rx/internal/operators/OperatorToMap.java b/src/main/java/rx/internal/operators/OperatorToMap.java index 5b81e071fb..0a1ea9fcd0 100644 --- a/src/main/java/rx/internal/operators/OperatorToMap.java +++ b/src/main/java/rx/internal/operators/OperatorToMap.java @@ -45,9 +45,9 @@ public Map call() { } - private final Func1 keySelector; + final Func1 keySelector; - private final Func1 valueSelector; + final Func1 valueSelector; private final Func0> mapFactory; diff --git a/src/main/java/rx/internal/operators/OperatorToMultimap.java b/src/main/java/rx/internal/operators/OperatorToMultimap.java index 6b840bed18..1f3423e02c 100644 --- a/src/main/java/rx/internal/operators/OperatorToMultimap.java +++ b/src/main/java/rx/internal/operators/OperatorToMultimap.java @@ -58,10 +58,10 @@ public Collection call(K t1) { } } - private final Func1 keySelector; - private final Func1 valueSelector; + final Func1 keySelector; + final Func1 valueSelector; private final Func0>> mapFactory; - private final Func1> collectionFactory; + final Func1> collectionFactory; /** * ToMultimap with key selector, custom value selector, diff --git a/src/main/java/rx/internal/operators/OperatorToObservableList.java b/src/main/java/rx/internal/operators/OperatorToObservableList.java index d2e9d717f6..66e0bb188a 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableList.java @@ -49,7 +49,7 @@ private static final class Holder { public static OperatorToObservableList instance() { return (OperatorToObservableList)Holder.INSTANCE; } - private OperatorToObservableList() { } + OperatorToObservableList() { } @Override public Subscriber call(final Subscriber> o) { final SingleDelayedProducer> producer = new SingleDelayedProducer>(o); diff --git a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java index 19246cbe7c..ab74e44f17 100644 --- a/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java +++ b/src/main/java/rx/internal/operators/OperatorToObservableSortedList.java @@ -34,8 +34,8 @@ * the type of the items emitted by the source and the resulting {@code Observable}s */ public final class OperatorToObservableSortedList implements Operator, T> { - private final Comparator sortFunction; - private final int initialCapacity; + final Comparator sortFunction; + final int initialCapacity; @SuppressWarnings("unchecked") public OperatorToObservableSortedList(int initialCapacity) { @@ -105,6 +105,8 @@ public void onNext(T value) { private static Comparator DEFAULT_SORT_FUNCTION = new DefaultComparableFunction(); private static class DefaultComparableFunction implements Comparator { + DefaultComparableFunction() { + } // unchecked because we want to support Object for this default @SuppressWarnings("unchecked") diff --git a/src/main/java/rx/internal/operators/OperatorUnsubscribeOn.java b/src/main/java/rx/internal/operators/OperatorUnsubscribeOn.java index 20957e29f4..327b4d1d24 100644 --- a/src/main/java/rx/internal/operators/OperatorUnsubscribeOn.java +++ b/src/main/java/rx/internal/operators/OperatorUnsubscribeOn.java @@ -27,7 +27,7 @@ */ public class OperatorUnsubscribeOn implements Operator { - private final Scheduler scheduler; + final Scheduler scheduler; public OperatorUnsubscribeOn(Scheduler scheduler) { this.scheduler = scheduler; diff --git a/src/main/java/rx/internal/operators/OperatorZip.java b/src/main/java/rx/internal/operators/OperatorZip.java index 2c17761b14..91fc05f09f 100644 --- a/src/main/java/rx/internal/operators/OperatorZip.java +++ b/src/main/java/rx/internal/operators/OperatorZip.java @@ -178,7 +178,7 @@ public void request(long n) { } private static final class Zip extends AtomicLong { - private final Observer child; + final Observer child; private final FuncN zipFunction; private final CompositeSubscription childSubscription = new CompositeSubscription(); diff --git a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java index 76d3f95926..afcf7464ed 100644 --- a/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java +++ b/src/main/java/rx/internal/schedulers/EventLoopsScheduler.java @@ -26,8 +26,8 @@ public class EventLoopsScheduler extends Scheduler implements SchedulerLifecycle { /** Manages a fixed number of workers. */ private static final String THREAD_NAME_PREFIX = "RxComputationThreadPool-"; - private static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); - /** + static final RxThreadFactory THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX); + /** * Key to setting the maximum number of computation scheduler threads. * Zero or less is interpreted as use available. Capped by available. */ @@ -172,8 +172,8 @@ public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { return poolWorker.scheduleActual(action, delayTime, unit, timed); } } - - private static final class PoolWorker extends NewThreadWorker { + + static final class PoolWorker extends NewThreadWorker { PoolWorker(ThreadFactory threadFactory) { super(threadFactory); } diff --git a/src/main/java/rx/internal/schedulers/ScheduledAction.java b/src/main/java/rx/internal/schedulers/ScheduledAction.java index 8ddd18870b..0f7d145a20 100644 --- a/src/main/java/rx/internal/schedulers/ScheduledAction.java +++ b/src/main/java/rx/internal/schedulers/ScheduledAction.java @@ -131,7 +131,7 @@ public void addParent(SubscriptionList parent) { private final class FutureCompleter implements Subscription { private final Future f; - private FutureCompleter(Future f) { + FutureCompleter(Future f) { this.f = f; } diff --git a/src/main/java/rx/internal/util/IndexedRingBuffer.java b/src/main/java/rx/internal/util/IndexedRingBuffer.java index f2fe163276..4bf941dee1 100644 --- a/src/main/java/rx/internal/util/IndexedRingBuffer.java +++ b/src/main/java/rx/internal/util/IndexedRingBuffer.java @@ -290,7 +290,7 @@ public void unsubscribe() { releaseToPool(); } - private IndexedRingBuffer() { + IndexedRingBuffer() { } /** @@ -483,8 +483,11 @@ private int forEach(Func1 action, int startIndex, int endInd } private static class ElementSection { - private final AtomicReferenceArray array = new AtomicReferenceArray(SIZE); - private final AtomicReference> next = new AtomicReference>(); + final AtomicReferenceArray array = new AtomicReferenceArray(SIZE); + final AtomicReference> next = new AtomicReference>(); + + ElementSection() { + } ElementSection getNext() { if (next.get() != null) { @@ -506,6 +509,9 @@ private static class IndexSection { private final AtomicIntegerArray unsafeArray = new AtomicIntegerArray(SIZE); + IndexSection() { + } + public int getAndSet(int expected, int newValue) { return unsafeArray.getAndSet(expected, newValue); } diff --git a/src/main/java/rx/internal/util/ObjectPool.java b/src/main/java/rx/internal/util/ObjectPool.java index 504c10cad4..0aa005208e 100644 --- a/src/main/java/rx/internal/util/ObjectPool.java +++ b/src/main/java/rx/internal/util/ObjectPool.java @@ -28,9 +28,9 @@ import rx.schedulers.Schedulers; public abstract class ObjectPool implements SchedulerLifecycle { - private Queue pool; - private final int minSize; - private final int maxSize; + Queue pool; + final int minSize; + final int maxSize; private final long validationInterval; private final AtomicReference schedulerWorker; diff --git a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java index 145a67096e..9a1dce56d7 100644 --- a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java +++ b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java @@ -29,7 +29,7 @@ public static final ScalarSynchronousObservable create(T t) { return new ScalarSynchronousObservable(t); } - private final T t; + final T t; protected ScalarSynchronousObservable(final T t) { super(new OnSubscribe() { @@ -103,7 +103,7 @@ static final class ScalarSynchronousAction implements Action0 { private final Subscriber subscriber; private final T value; - private ScalarSynchronousAction(Subscriber subscriber, + ScalarSynchronousAction(Subscriber subscriber, T value) { this.subscriber = subscriber; this.value = value; diff --git a/src/main/java/rx/internal/util/UtilityFunctions.java b/src/main/java/rx/internal/util/UtilityFunctions.java index 2a94cb95f9..2d233a4fb1 100644 --- a/src/main/java/rx/internal/util/UtilityFunctions.java +++ b/src/main/java/rx/internal/util/UtilityFunctions.java @@ -104,6 +104,9 @@ private static final class NullFunction, Func9, FuncN { + NullFunction() { + } + @Override public R call() { return null; diff --git a/src/main/java/rx/observables/AsyncOnSubscribe.java b/src/main/java/rx/observables/AsyncOnSubscribe.java index 61cbe79e7c..d95dc82b9d 100644 --- a/src/main/java/rx/observables/AsyncOnSubscribe.java +++ b/src/main/java/rx/observables/AsyncOnSubscribe.java @@ -264,7 +264,7 @@ private static final class AsyncOnSubscribeImpl extends AsyncOnSubscribe>, ? extends S> next; private final Action1 onUnsubscribe; - private AsyncOnSubscribeImpl(Func0 generator, Func3>, ? extends S> next, Action1 onUnsubscribe) { + AsyncOnSubscribeImpl(Func0 generator, Func3>, ? extends S> next, Action1 onUnsubscribe) { this.generator = generator; this.next = next; this.onUnsubscribe = onUnsubscribe; @@ -355,7 +355,7 @@ static final class AsyncOuterManager implements Producer, Subscription, Ob private final AsyncOnSubscribe parent; private final SerializedObserver> serializedSubscriber; - private final CompositeSubscription subscriptions = new CompositeSubscription(); + final CompositeSubscription subscriptions = new CompositeSubscription(); private boolean hasTerminated; private boolean onNextCalled; @@ -647,7 +647,7 @@ public void onNext(T t) { } static final class State implements OnSubscribe { - private Subscriber subscriber; + Subscriber subscriber; @Override public void call(Subscriber s) { synchronized (this) { diff --git a/src/main/java/rx/observables/BlockingObservable.java b/src/main/java/rx/observables/BlockingObservable.java index c1ded4c217..c5b3588e32 100644 --- a/src/main/java/rx/observables/BlockingObservable.java +++ b/src/main/java/rx/observables/BlockingObservable.java @@ -538,14 +538,14 @@ public void onCompleted() { } /** Constant to indicate the onStart method should be called. */ - private static final Object ON_START = new Object(); - + static final Object ON_START = new Object(); + /** Constant indicating the setProducer method should be called. */ - private static final Object SET_PRODUCER = new Object(); + static final Object SET_PRODUCER = new Object(); /** Indicates an unsubscripton happened */ - private static final Object UNSUBSCRIBE = new Object(); - + static final Object UNSUBSCRIBE = new Object(); + /** * Subscribes to the source and calls the Subscriber methods on the current thread. *

    diff --git a/src/main/java/rx/observables/SyncOnSubscribe.java b/src/main/java/rx/observables/SyncOnSubscribe.java index c75173a094..707e047b2a 100644 --- a/src/main/java/rx/observables/SyncOnSubscribe.java +++ b/src/main/java/rx/observables/SyncOnSubscribe.java @@ -271,7 +271,7 @@ private static final class SyncOnSubscribeImpl extends SyncOnSubscribe, ? extends S> next; private final Action1 onUnsubscribe; - private SyncOnSubscribeImpl(Func0 generator, Func2, ? extends S> next, Action1 onUnsubscribe) { + SyncOnSubscribeImpl(Func0 generator, Func2, ? extends S> next, Action1 onUnsubscribe) { this.generator = generator; this.next = next; this.onUnsubscribe = onUnsubscribe; @@ -322,8 +322,8 @@ private static class SubscriptionProducer private boolean hasTerminated; private S state; - - private SubscriptionProducer(final Subscriber subscriber, SyncOnSubscribe parent, S state) { + + SubscriptionProducer(final Subscriber subscriber, SyncOnSubscribe parent, S state) { this.actualSubscriber = subscriber; this.parent = parent; this.state = state; diff --git a/src/main/java/rx/schedulers/CachedThreadScheduler.java b/src/main/java/rx/schedulers/CachedThreadScheduler.java index 6ef56a17cb..31c6f9288f 100644 --- a/src/main/java/rx/schedulers/CachedThreadScheduler.java +++ b/src/main/java/rx/schedulers/CachedThreadScheduler.java @@ -26,11 +26,11 @@ /* package */final class CachedThreadScheduler extends Scheduler implements SchedulerLifecycle { private static final String WORKER_THREAD_NAME_PREFIX = "RxCachedThreadScheduler-"; - private static final RxThreadFactory WORKER_THREAD_FACTORY = + static final RxThreadFactory WORKER_THREAD_FACTORY = new RxThreadFactory(WORKER_THREAD_NAME_PREFIX); private static final String EVICTOR_THREAD_NAME_PREFIX = "RxCachedWorkerPoolEvictor-"; - private static final RxThreadFactory EVICTOR_THREAD_FACTORY = + static final RxThreadFactory EVICTOR_THREAD_FACTORY = new RxThreadFactory(EVICTOR_THREAD_NAME_PREFIX); private static final long KEEP_ALIVE_TIME = 60; diff --git a/src/main/java/rx/schedulers/ImmediateScheduler.java b/src/main/java/rx/schedulers/ImmediateScheduler.java index 4b9c27787f..e480754a58 100644 --- a/src/main/java/rx/schedulers/ImmediateScheduler.java +++ b/src/main/java/rx/schedulers/ImmediateScheduler.java @@ -45,6 +45,9 @@ private class InnerImmediateScheduler extends Scheduler.Worker implements Subscr 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 diff --git a/src/main/java/rx/schedulers/TestScheduler.java b/src/main/java/rx/schedulers/TestScheduler.java index c808a1a366..fec8bbcd75 100644 --- a/src/main/java/rx/schedulers/TestScheduler.java +++ b/src/main/java/rx/schedulers/TestScheduler.java @@ -31,17 +31,17 @@ * advancing the clock at whatever pace you choose. */ public class TestScheduler extends Scheduler { - private final Queue queue = new PriorityQueue(11, new CompareActionsByTime()); - private static long counter = 0; + final Queue queue = new PriorityQueue(11, new CompareActionsByTime()); + static long counter = 0; private static final class TimedAction { - private final long time; - private final Action0 action; - private final Worker scheduler; + final long time; + final Action0 action; + final Worker scheduler; private final long count = counter++; // for differentiating tasks at same time - private TimedAction(Worker scheduler, long time, Action0 action) { + TimedAction(Worker scheduler, long time, Action0 action) { this.time = time; this.action = action; this.scheduler = scheduler; @@ -54,6 +54,9 @@ public String toString() { } private static class CompareActionsByTime implements Comparator { + CompareActionsByTime() { + } + @Override public int compare(TimedAction action1, TimedAction action2) { if (action1.time == action2.time) { @@ -65,7 +68,7 @@ public int compare(TimedAction action1, TimedAction action2) { } // Storing time in nanoseconds internally. - private long time; + long time; @Override public long now() { @@ -132,6 +135,9 @@ private final class InnerTestScheduler extends Worker { private final BooleanSubscription s = new BooleanSubscription(); + InnerTestScheduler() { + } + @Override public void unsubscribe() { s.unsubscribe(); diff --git a/src/main/java/rx/schedulers/TrampolineScheduler.java b/src/main/java/rx/schedulers/TrampolineScheduler.java index 9f7b14eb43..45bb18546c 100644 --- a/src/main/java/rx/schedulers/TrampolineScheduler.java +++ b/src/main/java/rx/schedulers/TrampolineScheduler.java @@ -47,10 +47,13 @@ public Worker createWorker() { private static class InnerCurrentThreadScheduler extends Scheduler.Worker implements Subscription { final AtomicInteger counter = new AtomicInteger(); - private final PriorityBlockingQueue queue = new PriorityBlockingQueue(); + 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()); @@ -108,7 +111,7 @@ private static final class TimedAction implements Comparable { final Long execTime; final int count; // In case if time between enqueueing took less than 1ms - private TimedAction(Action0 action, Long execTime, int count) { + TimedAction(Action0 action, Long execTime, int count) { this.action = action; this.execTime = execTime; this.count = count; @@ -125,7 +128,7 @@ public int compareTo(TimedAction that) { } // because I can't use Integer.compare from Java 7 - private static int compare(int x, int y) { + static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); } diff --git a/src/main/java/rx/subjects/TestSubject.java b/src/main/java/rx/subjects/TestSubject.java index 2cc32b007c..f7a0caee2a 100644 --- a/src/main/java/rx/subjects/TestSubject.java +++ b/src/main/java/rx/subjects/TestSubject.java @@ -75,7 +75,7 @@ public void onCompleted() { onCompleted(0); } - private void _onCompleted() { + void _onCompleted() { if (state.active) { for (SubjectObserver bo : state.terminate(NotificationLite.instance().completed())) { bo.onCompleted(); @@ -108,7 +108,7 @@ public void onError(final Throwable e) { onError(e, 0); } - private void _onError(final Throwable e) { + void _onError(final Throwable e) { if (state.active) { for (SubjectObserver bo : state.terminate(NotificationLite.instance().error(e))) { bo.onError(e); @@ -143,7 +143,7 @@ public void onNext(T v) { onNext(v, 0); } - private void _onNext(T v) { + void _onNext(T v) { for (Observer o : state.observers()) { o.onNext(v); } diff --git a/src/main/java/rx/subscriptions/Subscriptions.java b/src/main/java/rx/subscriptions/Subscriptions.java index a86f5ef090..64d941f13d 100644 --- a/src/main/java/rx/subscriptions/Subscriptions.java +++ b/src/main/java/rx/subscriptions/Subscriptions.java @@ -120,7 +120,7 @@ public static CompositeSubscription from(Subscription... subscriptions) { */ private static final Unsubscribed UNSUBSCRIBED = new Unsubscribed(); /** Naming classes helps with debugging. */ - private static final class Unsubscribed implements Subscription { + static final class Unsubscribed implements Subscription { @Override public void unsubscribe() { } From 97dbfedc43dbb9e4cc1eb085bc54f4f9b1b85f0e Mon Sep 17 00:00:00 2001 From: Shixiong Zhu Date: Mon, 21 Dec 2015 18:17:17 -0800 Subject: [PATCH 476/857] Fix the initialization order in GenericScheduledExecutorService The static `GenericScheduledExecutorService.None` should be initialized before creating any GenericScheduledExecutorService instance. --- .../GenericScheduledExecutorService.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java index 8d0d5bdec2..82260207ae 100644 --- a/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java +++ b/src/main/java/rx/internal/schedulers/GenericScheduledExecutorService.java @@ -35,18 +35,18 @@ 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); - - /* Schedulers needs acces to this in order to work with the lifecycle. */ - public final static GenericScheduledExecutorService INSTANCE = new GenericScheduledExecutorService(); - - private final AtomicReference executor; - - static final ScheduledExecutorService NONE; + + private static final ScheduledExecutorService NONE; static { NONE = Executors.newScheduledThreadPool(0); NONE.shutdownNow(); } + + /* Schedulers needs acces to this in order to work with the lifecycle. */ + public final static GenericScheduledExecutorService INSTANCE = new GenericScheduledExecutorService(); + private final AtomicReference executor; + private GenericScheduledExecutorService() { executor = new AtomicReference(NONE); start(); From 1b24634a52473c66da2d99f15f1f32aa12cfc298 Mon Sep 17 00:00:00 2001 From: mushuichuan Date: Wed, 30 Dec 2015 10:46:41 +0800 Subject: [PATCH 477/857] add never test for PublishSuibjectText --- .../java/rx/subjects/PublishSubjectTest.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/test/java/rx/subjects/PublishSubjectTest.java b/src/test/java/rx/subjects/PublishSubjectTest.java index 7b3248d8d7..a463a48118 100644 --- a/src/test/java/rx/subjects/PublishSubjectTest.java +++ b/src/test/java/rx/subjects/PublishSubjectTest.java @@ -63,7 +63,7 @@ public void testCompleted() { subject.onError(new Throwable()); assertCompletedObserver(observer); - // todo bug? assertNeverObserver(anotherObserver); + assertNeverObserver(anotherObserver); } @Test @@ -113,6 +113,16 @@ private void assertCompletedObserver(Observer observer) { verify(observer, times(1)).onNext("one"); verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); + verify(observer, never()).onNext("four"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + } + + private void assertNeverObserver(Observer observer) { + verify(observer, never()).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, never()).onNext("three"); + verify(observer, never()).onNext("four"); verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @@ -139,7 +149,7 @@ public void testError() { subject.onCompleted(); assertErrorObserver(observer); - // todo bug? assertNeverObserver(anotherObserver); + assertNeverErrorObserver(anotherObserver); } private void assertErrorObserver(Observer observer) { @@ -150,6 +160,15 @@ private void assertErrorObserver(Observer observer) { verify(observer, never()).onCompleted(); } + private void assertNeverErrorObserver(Observer observer) { + verify(observer, never()).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, never()).onNext("three"); + verify(observer, never()).onNext("four"); + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onCompleted(); + } + @Test public void testSubscribeMidSequence() { PublishSubject subject = PublishSubject.create(); From fe75d053a8b4a8badadfef608f359eed579e81f1 Mon Sep 17 00:00:00 2001 From: mushuichuan Date: Wed, 30 Dec 2015 15:05:23 +0800 Subject: [PATCH 478/857] add verifyNoMoreInteractions(observer) for PublishSuibjectText --- src/test/java/rx/subjects/PublishSubjectTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/rx/subjects/PublishSubjectTest.java b/src/test/java/rx/subjects/PublishSubjectTest.java index a463a48118..93c9be4bd3 100644 --- a/src/test/java/rx/subjects/PublishSubjectTest.java +++ b/src/test/java/rx/subjects/PublishSubjectTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; @@ -116,6 +117,7 @@ private void assertCompletedObserver(Observer observer) { verify(observer, never()).onNext("four"); verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); + verifyNoMoreInteractions(observer); } private void assertNeverObserver(Observer observer) { @@ -125,6 +127,7 @@ private void assertNeverObserver(Observer observer) { verify(observer, never()).onNext("four"); verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); + verifyNoMoreInteractions(observer); } @Test @@ -158,6 +161,7 @@ private void assertErrorObserver(Observer observer) { verify(observer, times(1)).onNext("three"); verify(observer, times(1)).onError(testException); verify(observer, never()).onCompleted(); + verifyNoMoreInteractions(observer); } private void assertNeverErrorObserver(Observer observer) { @@ -167,6 +171,7 @@ private void assertNeverErrorObserver(Observer observer) { verify(observer, never()).onNext("four"); verify(observer, times(1)).onError(any(Throwable.class)); verify(observer, never()).onCompleted(); + verifyNoMoreInteractions(observer); } @Test From acaf42e94eead32c1137b1d72b1ec7c84fb0fb6c Mon Sep 17 00:00:00 2001 From: Aaron Tull Date: Wed, 9 Dec 2015 12:41:53 -0800 Subject: [PATCH 479/857] Implemented Completable#andThen(Observable) --- src/main/java/rx/Completable.java | 15 +++++++ src/test/java/rx/CompletableTest.java | 59 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index aa222b9e8e..80a1ea5b25 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -1080,6 +1080,21 @@ public final Completable compose(CompletableTransformer transformer) { return to(transformer); } + /** + * Returns an Observable which will subscribe to this Completable and once that is completed then + * will subscribe to the {@code next} Observable. An error event from this Completable will be + * propagated to the downstream subscriber and will result in skipping the subscription of the + * Observable. + * + * @param next the Observable to subscribe after this Completable is completed, not null + * @return Observable that composes this Completable and next + * @throws NullPointerException if next is null + */ + public final Observable andThen(Observable next) { + requireNonNull(next); + return next.delaySubscription(toObservable()); + } + /** * Concatenates this Completable with another Completable. * @param other the other Completable, not null diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index 09d71a9ff7..1fca0df0e0 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -23,6 +23,7 @@ import org.junit.*; import rx.Completable.*; +import rx.Observable.OnSubscribe; import rx.exceptions.*; import rx.functions.*; import rx.observers.TestSubscriber; @@ -357,6 +358,64 @@ public void call(Long v) { Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); } + @Test + public void andThen() { + TestSubscriber ts = new TestSubscriber(0); + Completable.complete().andThen(Observable.just("foo")).subscribe(ts); + ts.requestMore(1); + ts.assertValue("foo"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void andThenNever() { + TestSubscriber ts = new TestSubscriber(0); + Completable.never().andThen(Observable.just("foo")).subscribe(ts); + ts.requestMore(1); + ts.assertNoValues(); + ts.assertNoTerminalEvent(); + } + + @Test + public void andThenError() { + TestSubscriber ts = new TestSubscriber(0); + final AtomicBoolean hasRun = new AtomicBoolean(false); + final Exception e = new Exception(); + Completable.create(new CompletableOnSubscribe() { + @Override + public void call(CompletableSubscriber cs) { + cs.onError(e); + } + }) + .andThen(Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber s) { + hasRun.set(true); + s.onNext("foo"); + s.onCompleted(); + } + })) + .subscribe(ts); + ts.assertNoValues(); + ts.assertError(e); + Assert.assertFalse("Should not have subscribed to observable when completable errors", hasRun.get()); + } + + @Test + public void andThenSubscribeOn() { + TestSubscriber ts = new TestSubscriber(0); + TestScheduler scheduler = new TestScheduler(); + Completable.complete().andThen(Observable.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(); + } + @Test(expected = NullPointerException.class) public void createNull() { Completable.create(null); From 51451b0bb06388e3f3c9fc227ff500541e5a37e0 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Fri, 8 Jan 2016 17:39:05 -0500 Subject: [PATCH 480/857] delaySubscription(Func0) does not use a scheduler It subscribes to the upstream `Observable` on the emitting thread of the other `Observable` obtained from the `Func0`. --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 2468cb5145..76d6afbcec 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -4218,7 +4218,7 @@ public final Observable delaySubscription(long delay, TimeUnit unit, Schedule * *

    *
    Scheduler:
    - *
    This version of {@code delay} operates by default on the {@code compuation} {@link Scheduler}.
    + *
    This method does not operate by default on a particular {@link Scheduler}.
    *
    * * @param subscriptionDelay From c925e860c01c30edc15c59c592c1d5e9b9777a90 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 13 Jan 2016 13:59:04 +0100 Subject: [PATCH 481/857] 1.x: just() now supports backpressure (+ related fixes/changes) --- src/main/java/rx/Observable.java | 2 +- src/main/java/rx/Single.java | 39 ++- .../operators/OperatorSubscribeOn.java | 124 ++++---- .../operators/OperatorTimeoutBase.java | 111 ++++--- .../util/ScalarSynchronousObservable.java | 280 +++++++++++++----- .../operators/OperatorReplayTest.java | 6 +- .../operators/OperatorUnsubscribeOnTest.java | 43 ++- .../util/ScalarSynchronousObservableTest.java | 233 +++++++++++++++ 8 files changed, 641 insertions(+), 197 deletions(-) create mode 100644 src/test/java/rx/internal/util/ScalarSynchronousObservableTest.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 9287e105de..8e94b3fc0d 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -8330,7 +8330,7 @@ public final Observable subscribeOn(Scheduler scheduler) { if (this instanceof ScalarSynchronousObservable) { return ((ScalarSynchronousObservable)this).scalarScheduleOn(scheduler); } - return nest().lift(new OperatorSubscribeOn(scheduler)); + return create(new OperatorSubscribeOn(this, scheduler)); } /** diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 2d332e5de8..1df2927d30 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1698,8 +1698,43 @@ public void onNext(T t) { * @see RxJava Threading Examples * @see #observeOn */ - public final Single subscribeOn(Scheduler scheduler) { - return nest().lift(new OperatorSubscribeOn(scheduler)); + public final Single subscribeOn(final Scheduler scheduler) { + return create(new OnSubscribe() { + @Override + public void call(final SingleSubscriber t) { + final Scheduler.Worker w = scheduler.createWorker(); + t.add(w); + + w.schedule(new Action0() { + @Override + public void call() { + SingleSubscriber ssub = new SingleSubscriber() { + @Override + public void onSuccess(T value) { + try { + t.onSuccess(value); + } finally { + w.unsubscribe(); + } + } + + @Override + public void onError(Throwable error) { + try { + t.onError(error); + } finally { + w.unsubscribe(); + } + } + }; + + t.add(ssub); + + Single.this.subscribe(ssub); + } + }); + } + }); } /** diff --git a/src/main/java/rx/internal/operators/OperatorSubscribeOn.java b/src/main/java/rx/internal/operators/OperatorSubscribeOn.java index 152bc504e4..70bc2fa592 100644 --- a/src/main/java/rx/internal/operators/OperatorSubscribeOn.java +++ b/src/main/java/rx/internal/operators/OperatorSubscribeOn.java @@ -15,96 +15,84 @@ */ package rx.internal.operators; -import rx.Observable; -import rx.Observable.Operator; -import rx.Producer; -import rx.Scheduler; +import rx.*; +import rx.Observable.OnSubscribe; import rx.Scheduler.Worker; -import rx.Subscriber; import rx.functions.Action0; /** * Subscribes Observers on the specified {@code Scheduler}. *

    * + * + * @param the value type of the actual source */ -public class OperatorSubscribeOn implements Operator> { +public final class OperatorSubscribeOn implements OnSubscribe { - private final Scheduler scheduler; + final Scheduler scheduler; + final Observable source; - public OperatorSubscribeOn(Scheduler scheduler) { + public OperatorSubscribeOn(Observable source, Scheduler scheduler) { this.scheduler = scheduler; + this.source = source; } @Override - public Subscriber> call(final Subscriber subscriber) { + public void call(final Subscriber subscriber) { final Worker inner = scheduler.createWorker(); subscriber.add(inner); - return new Subscriber>(subscriber) { - - @Override - public void onCompleted() { - // ignore because this is a nested Observable and we expect only 1 Observable emitted to onNext - } - - @Override - public void onError(Throwable e) { - subscriber.onError(e); - } - + + inner.schedule(new Action0() { @Override - public void onNext(final Observable o) { - inner.schedule(new Action0() { - + public void call() { + final Thread t = Thread.currentThread(); + + Subscriber s = new Subscriber(subscriber) { @Override - public void call() { - final Thread t = Thread.currentThread(); - o.unsafeSubscribe(new Subscriber(subscriber) { - - @Override - public void onCompleted() { - subscriber.onCompleted(); - } - - @Override - public void onError(Throwable e) { - subscriber.onError(e); - } - - @Override - public void onNext(T t) { - subscriber.onNext(t); - } - + public void onNext(T t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + try { + subscriber.onError(e); + } finally { + inner.unsubscribe(); + } + } + + @Override + public void onCompleted() { + try { + subscriber.onCompleted(); + } finally { + inner.unsubscribe(); + } + } + + @Override + public void setProducer(final Producer p) { + subscriber.setProducer(new Producer() { @Override - public void setProducer(final Producer producer) { - subscriber.setProducer(new Producer() { - - @Override - public void request(final long n) { - if (Thread.currentThread() == t) { - // don't schedule if we're already on the thread (primarily for first setProducer call) - // see unit test 'testSetProducerSynchronousRequest' for more context on this - producer.request(n); - } else { - inner.schedule(new Action0() { - - @Override - public void call() { - producer.request(n); - } - }); + public void request(final long n) { + if (t == Thread.currentThread()) { + p.request(n); + } else { + inner.schedule(new Action0() { + @Override + public void call() { + p.request(n); } - } - - }); + }); + } } - }); } - }); + }; + + source.unsafeSubscribe(s); } - - }; + }); } -} +} \ No newline at end of file diff --git a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java index d4700bcb9b..823831bc3a 100644 --- a/src/main/java/rx/internal/operators/OperatorTimeoutBase.java +++ b/src/main/java/rx/internal/operators/OperatorTimeoutBase.java @@ -16,16 +16,11 @@ package rx.internal.operators; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import rx.Observable; +import rx.*; import rx.Observable.Operator; -import rx.Scheduler; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Func3; -import rx.functions.Func4; +import rx.functions.*; +import rx.internal.producers.ProducerArbiter; import rx.observers.SerializedSubscriber; import rx.subscriptions.SerialSubscription; @@ -49,10 +44,10 @@ class OperatorTimeoutBase implements Operator { Func4, Long, T, Scheduler.Worker, Subscription> { } - private final FirstTimeoutStub firstTimeoutStub; - private final TimeoutStub timeoutStub; - private final Observable other; - private final Scheduler scheduler; + final FirstTimeoutStub firstTimeoutStub; + final TimeoutStub timeoutStub; + final Observable other; + final Scheduler scheduler; /* package-private */OperatorTimeoutBase(FirstTimeoutStub firstTimeoutStub, TimeoutStub timeoutStub, Observable other, Scheduler scheduler) { this.firstTimeoutStub = firstTimeoutStub; @@ -65,67 +60,86 @@ class OperatorTimeoutBase implements Operator { public Subscriber call(Subscriber subscriber) { Scheduler.Worker inner = scheduler.createWorker(); subscriber.add(inner); - final SerialSubscription serial = new SerialSubscription(); - subscriber.add(serial); // Use SynchronizedSubscriber for safe memory access // as the subscriber will be accessed in the current thread or the // scheduler or other Observables. final SerializedSubscriber synchronizedSubscriber = new SerializedSubscriber(subscriber); + final SerialSubscription serial = new SerialSubscription(); + synchronizedSubscriber.add(serial); + TimeoutSubscriber timeoutSubscriber = new TimeoutSubscriber(synchronizedSubscriber, timeoutStub, serial, other, inner); + + synchronizedSubscriber.add(timeoutSubscriber); + synchronizedSubscriber.setProducer(timeoutSubscriber.arbiter); + serial.set(firstTimeoutStub.call(timeoutSubscriber, 0L, inner)); + return timeoutSubscriber; } /* package-private */static final class TimeoutSubscriber extends Subscriber { - private final SerialSubscription serial; - private final Object gate = new Object(); + final SerialSubscription serial; - private final SerializedSubscriber serializedSubscriber; + final SerializedSubscriber serializedSubscriber; - private final TimeoutStub timeoutStub; + final TimeoutStub timeoutStub; - private final Observable other; - private final Scheduler.Worker inner; + final Observable other; + + final Scheduler.Worker inner; + + final ProducerArbiter arbiter; + + /** Guarded by this. */ + boolean terminated; + /** Guarded by this. */ + long actual; - final AtomicInteger terminated = new AtomicInteger(); - final AtomicLong actual = new AtomicLong(); - TimeoutSubscriber( SerializedSubscriber serializedSubscriber, TimeoutStub timeoutStub, SerialSubscription serial, Observable other, Scheduler.Worker inner) { - super(serializedSubscriber); this.serializedSubscriber = serializedSubscriber; this.timeoutStub = timeoutStub; this.serial = serial; this.other = other; this.inner = inner; + this.arbiter = new ProducerArbiter(); } + @Override + public void setProducer(Producer p) { + arbiter.setProducer(p); + } + @Override public void onNext(T value) { boolean onNextWins = false; - synchronized (gate) { - if (terminated.get() == 0) { - actual.incrementAndGet(); + long a; + synchronized (this) { + if (!terminated) { + a = ++actual; onNextWins = true; + } else { + a = actual; } } if (onNextWins) { serializedSubscriber.onNext(value); - serial.set(timeoutStub.call(this, actual.get(), value, inner)); + serial.set(timeoutStub.call(this, a, value, inner)); } } @Override public void onError(Throwable error) { boolean onErrorWins = false; - synchronized (gate) { - if (terminated.getAndSet(1) == 0) { + synchronized (this) { + if (!terminated) { + terminated = true; onErrorWins = true; } } @@ -138,8 +152,9 @@ public void onError(Throwable error) { @Override public void onCompleted() { boolean onCompletedWins = false; - synchronized (gate) { - if (terminated.getAndSet(1) == 0) { + synchronized (this) { + if (!terminated) { + terminated = true; onCompletedWins = true; } } @@ -152,8 +167,9 @@ public void onCompleted() { public void onTimeout(long seqId) { long expected = seqId; boolean timeoutWins = false; - synchronized (gate) { - if (expected == actual.get() && terminated.getAndSet(1) == 0) { + synchronized (this) { + if (expected == actual && !terminated) { + terminated = true; timeoutWins = true; } } @@ -161,10 +177,31 @@ public void onTimeout(long seqId) { if (other == null) { serializedSubscriber.onError(new TimeoutException()); } else { - other.unsafeSubscribe(serializedSubscriber); - serial.set(serializedSubscriber); + Subscriber second = new Subscriber() { + @Override + public void onNext(T t) { + serializedSubscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + serializedSubscriber.onError(e); + } + + @Override + public void onCompleted() { + serializedSubscriber.onCompleted(); + } + + @Override + public void setProducer(Producer p) { + arbiter.setProducer(p); + } + }; + other.unsafeSubscribe(second); + serial.set(second); } } } } -} +} \ No newline at end of file diff --git a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java index 9a1dce56d7..797a4e4406 100644 --- a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java +++ b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java @@ -15,20 +15,74 @@ */ package rx.internal.util; -import rx.Observable; -import rx.Scheduler; -import rx.Scheduler.Worker; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Func1; +import java.util.concurrent.atomic.AtomicBoolean; + +import rx.*; +import rx.exceptions.Exceptions; +import rx.functions.*; +import rx.internal.producers.SingleProducer; import rx.internal.schedulers.EventLoopsScheduler; +import rx.observers.Subscribers; +import rx.schedulers.Schedulers; +/** + * An Observable that emits a single constant scalar value to Subscribers. + *

    + * This is a direct implementation of the Observable class to allow identifying it + * in flatMap and bypass the subscription to it altogether. + * + * @param the value type + */ public final class ScalarSynchronousObservable extends Observable { + /** + * We expect the Schedulers.computation() to return an EventLoopsScheduler all the time. + */ + static final Func1 COMPUTATION_ONSCHEDULE = new Func1() { + final EventLoopsScheduler els = (EventLoopsScheduler)Schedulers.computation(); + + @Override + public Subscription call(Action0 t) { + return els.scheduleDirect(t); + } + }; + + /** + * Indicates that the Producer used by this Observable should be fully + * threadsafe. It is possible, but unlikely that multiple concurrent + * requests will arrive to just(). + */ + static final boolean STRONG_MODE; + static { + String wp = System.getProperty("rx.just.strong-mode", "false"); + STRONG_MODE = Boolean.valueOf(wp); + } + + /** + * Creates a scalar producer depending on the state of STRONG_MODE. + * @param the type of the scalar value + * @param s the target subscriber + * @param v the value to emit + * @return the created Producer + */ + static Producer createProducer(Subscriber s, T v) { + if (STRONG_MODE) { + return new SingleProducer(s, v); + } + return new WeakSingleProducer(s, v); + } + + /** + * Constructs a ScalarSynchronousObservable with the given constant value. + * @param the value type + * @param t the value to emit when requested + * @return the new Observable + */ public static final ScalarSynchronousObservable create(T t) { return new ScalarSynchronousObservable(t); } + /** The constant scalar value to emit on request. */ final T t; protected ScalarSynchronousObservable(final T t) { @@ -36,116 +90,198 @@ protected ScalarSynchronousObservable(final T t) { @Override public void call(Subscriber s) { - /* - * We don't check isUnsubscribed as it is a significant performance impact in the fast-path use cases. - * See PerfBaseline tests and https://github.com/ReactiveX/RxJava/issues/1383 for more information. - * The assumption here is that when asking for a single item we should emit it and not concern ourselves with - * being unsubscribed already. If the Subscriber unsubscribes at 0, they shouldn't have subscribed, or it will - * filter it out (such as take(0)). This prevents us from paying the price on every subscription. - */ - s.onNext(t); - s.onCompleted(); + s.setProducer(createProducer(s, t)); } }); this.t = t; } + /** + * Returns the scalar constant value directly. + * @return the scalar constant value directly + */ public T get() { return t; } + + /** * Customized observeOn/subscribeOn implementation which emits the scalar * value directly or with less overhead on the specified scheduler. * @param scheduler the target scheduler * @return the new observable */ - public Observable scalarScheduleOn(Scheduler scheduler) { + public Observable scalarScheduleOn(final Scheduler scheduler) { + final Func1 onSchedule; if (scheduler instanceof EventLoopsScheduler) { - EventLoopsScheduler es = (EventLoopsScheduler) scheduler; - return create(new DirectScheduledEmission(es, t)); + onSchedule = COMPUTATION_ONSCHEDULE; + } else { + onSchedule = new Func1() { + @Override + public Subscription call(final Action0 a) { + final Scheduler.Worker w = scheduler.createWorker(); + w.schedule(new Action0() { + @Override + public void call() { + try { + a.call(); + } finally { + w.unsubscribe(); + } + } + }); + return w; + } + }; } - return create(new NormalScheduledEmission(scheduler, t)); + + return create(new ScalarAsyncOnSubscribe(t, onSchedule)); } - /** Optimized observeOn for scalar value observed on the EventLoopsScheduler. */ - static final class DirectScheduledEmission implements OnSubscribe { - private final EventLoopsScheduler es; - private final T value; - DirectScheduledEmission(EventLoopsScheduler es, T value) { - this.es = es; - this.value = value; - } - @Override - public void call(final Subscriber child) { - child.add(es.scheduleDirect(new ScalarSynchronousAction(child, value))); - } - } - /** Emits a scalar value on a general scheduler. */ - static final class NormalScheduledEmission implements OnSubscribe { - private final Scheduler scheduler; - private final T value; + /** + * The OnSubscribe implementation that creates the ScalarAsyncProducer for each + * incoming subscriber. + * + * @param the value type + */ + static final class ScalarAsyncOnSubscribe implements OnSubscribe { + final T value; + final Func1 onSchedule; - NormalScheduledEmission(Scheduler scheduler, T value) { - this.scheduler = scheduler; + ScalarAsyncOnSubscribe(T value, Func1 onSchedule) { this.value = value; + this.onSchedule = onSchedule; } - + @Override - public void call(final Subscriber subscriber) { - Worker worker = scheduler.createWorker(); - subscriber.add(worker); - worker.schedule(new ScalarSynchronousAction(subscriber, value)); + public void call(Subscriber s) { + s.setProducer(new ScalarAsyncProducer(s, value, onSchedule)); } } - /** Action that emits a single value when called. */ - static final class ScalarSynchronousAction implements Action0 { - private final Subscriber subscriber; - private final T value; - ScalarSynchronousAction(Subscriber subscriber, - T value) { - this.subscriber = subscriber; + /** + * Represents a producer which schedules the emission of a scalar value on + * the first positive request via the given scheduler callback. + * + * @param the value type + */ + static final class ScalarAsyncProducer extends AtomicBoolean implements Producer, Action0 { + /** */ + private static final long serialVersionUID = -2466317989629281651L; + final Subscriber actual; + final T value; + final Func1 onSchedule; + + public ScalarAsyncProducer(Subscriber actual, T value, Func1 onSchedule) { + this.actual = actual; this.value = value; + this.onSchedule = onSchedule; } + @Override + public void request(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } + if (n != 0 && compareAndSet(false, true)) { + actual.add(onSchedule.call(this)); + } + } + @Override public void call() { + Subscriber a = actual; + if (a.isUnsubscribed()) { + return; + } + T v = value; try { - subscriber.onNext(value); - } catch (Throwable t) { - subscriber.onError(t); + a.onNext(v); + } catch (Throwable e) { + Exceptions.throwOrReport(e, a, v); + return; + } + if (a.isUnsubscribed()) { return; } - subscriber.onCompleted(); + a.onCompleted(); + } + + @Override + public String toString() { + return "ScalarAsyncProducer[" + value + ", " + get() + "]"; } } + /** + * Given this scalar source as input to a flatMap, avoid one step of subscription + * and subscribes to the single Observable returned by the function. + *

    + * If the functions returns another scalar, no subscription happens and this inner + * scalar value will be emitted once requested. + * @param the result type + * @param func the mapper function that returns an Observable for the scalar value of this + * @return the new observable + */ public Observable scalarFlatMap(final Func1> func) { return create(new OnSubscribe() { @Override public void call(final Subscriber child) { Observable o = func.call(t); - if (o.getClass() == ScalarSynchronousObservable.class) { - child.onNext(((ScalarSynchronousObservable)o).t); - child.onCompleted(); + if (o instanceof ScalarSynchronousObservable) { + child.setProducer(createProducer(child, ((ScalarSynchronousObservable)o).t)); } else { - o.unsafeSubscribe(new Subscriber(child) { - @Override - public void onNext(R v) { - child.onNext(v); - } - @Override - public void onError(Throwable e) { - child.onError(e); - } - @Override - public void onCompleted() { - child.onCompleted(); - } - }); + o.unsafeSubscribe(Subscribers.wrap(child)); } } }); } -} + + /** + * This is the weak version of SingleProducer that uses plain fields + * to avoid reentrancy and as such is not threadsafe for concurrent + * request() calls. + * + * @param the value type + */ + static final class WeakSingleProducer implements Producer { + final Subscriber actual; + final T value; + boolean once; + + public WeakSingleProducer(Subscriber actual, T value) { + this.actual = actual; + this.value = value; + } + + @Override + public void request(long n) { + if (once) { + return; + } + if (n < 0L) { + throw new IllegalStateException("n >= required but it was " + n); + } + if (n != 0L) { + once = true; + Subscriber a = actual; + if (a.isUnsubscribed()) { + return; + } + T v = value; + try { + a.onNext(v); + } catch (Throwable e) { + Exceptions.throwOrReport(e, a, v); + return; + } + + if (a.isUnsubscribed()) { + return; + } + a.onCompleted(); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/rx/internal/operators/OperatorReplayTest.java b/src/test/java/rx/internal/operators/OperatorReplayTest.java index 3da35b83b8..b05a6f3a72 100644 --- a/src/test/java/rx/internal/operators/OperatorReplayTest.java +++ b/src/test/java/rx/internal/operators/OperatorReplayTest.java @@ -623,7 +623,8 @@ public void testIssue2191_SchedulerUnsubscribe() throws Exception { verifyObserverMock(mockObserverAfterConnect, 2, 6); verify(spiedWorker, times(1)).isUnsubscribed(); - verify(spiedWorker, times(1)).unsubscribe(); + // subscribeOn didn't unsubscribe the worker before but it should have + verify(spiedWorker, times(2)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); verifyNoMoreInteractions(sourceNext); @@ -684,7 +685,8 @@ public void testIssue2191_SchedulerUnsubscribeOnError() throws Exception { verifyObserver(mockObserverAfterConnect, 2, 2, illegalArgumentException); verify(spiedWorker, times(1)).isUnsubscribed(); - verify(spiedWorker, times(1)).unsubscribe(); + // subscribeOn didn't unsubscribe the worker before but it should have + verify(spiedWorker, times(2)).unsubscribe(); verify(sourceUnsubscribed, times(1)).call(); verifyNoMoreInteractions(sourceNext); diff --git a/src/test/java/rx/internal/operators/OperatorUnsubscribeOnTest.java b/src/test/java/rx/internal/operators/OperatorUnsubscribeOnTest.java index 08bce82609..4be8b96298 100644 --- a/src/test/java/rx/internal/operators/OperatorUnsubscribeOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorUnsubscribeOnTest.java @@ -20,7 +20,7 @@ import static org.junit.Assert.assertTrue; import java.util.Arrays; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -31,13 +31,14 @@ import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; +import rx.internal.util.RxThreadFactory; import rx.observers.TestObserver; import rx.schedulers.Schedulers; import rx.subscriptions.Subscriptions; public class OperatorUnsubscribeOnTest { - @Test + @Test(timeout = 1000) public void testUnsubscribeWhenSubscribeOnAndUnsubscribeOnAreOnSameThread() throws InterruptedException { UIEventLoopScheduler UI_EVENT_LOOP = new UIEventLoopScheduler(); try { @@ -56,7 +57,11 @@ public void call(Subscriber t1) { }); TestObserver observer = new TestObserver(); - w.subscribeOn(UI_EVENT_LOOP).observeOn(Schedulers.computation()).unsubscribeOn(UI_EVENT_LOOP).subscribe(observer); + w + .subscribeOn(UI_EVENT_LOOP) + .observeOn(Schedulers.computation()) + .unsubscribeOn(UI_EVENT_LOOP) + .subscribe(observer); Thread unsubscribeThread = subscription.getThread(); @@ -78,7 +83,7 @@ public void call(Subscriber t1) { } } - @Test + @Test(timeout = 1000) public void testUnsubscribeWhenSubscribeOnAndUnsubscribeOnAreOnDifferentThreads() throws InterruptedException { UIEventLoopScheduler UI_EVENT_LOOP = new UIEventLoopScheduler(); try { @@ -97,7 +102,11 @@ public void call(Subscriber t1) { }); TestObserver observer = new TestObserver(); - w.subscribeOn(Schedulers.newThread()).observeOn(Schedulers.computation()).unsubscribeOn(UI_EVENT_LOOP).subscribe(observer); + w + .subscribeOn(Schedulers.newThread()) + .observeOn(Schedulers.computation()) + .unsubscribeOn(UI_EVENT_LOOP) + .subscribe(observer); Thread unsubscribeThread = subscription.getThread(); @@ -110,7 +119,10 @@ public void call(Subscriber t1) { System.out.println("unsubscribeThread: " + unsubscribeThread); System.out.println("subscribeThread.get(): " + subscribeThread.get()); - assertTrue(unsubscribeThread == UI_EVENT_LOOP.getThread()); + Thread uiThread = UI_EVENT_LOOP.getThread(); + System.out.println("UI_EVENT_LOOP: " + uiThread); + + assertTrue(unsubscribeThread == uiThread); observer.assertReceivedOnNext(Arrays.asList(1, 2)); observer.assertTerminalEvent(); @@ -153,23 +165,24 @@ public Thread getThread() throws InterruptedException { public static class UIEventLoopScheduler extends Scheduler { - private final Scheduler.Worker eventLoop; - private final Subscription s; + private final ExecutorService eventLoop; + final Scheduler single; private volatile Thread t; public UIEventLoopScheduler() { - eventLoop = Schedulers.newThread().createWorker(); - s = eventLoop; + eventLoop = Executors.newSingleThreadExecutor(new RxThreadFactory("Test-EventLoop")); + single = Schedulers.from(eventLoop); + /* * DON'T DO THIS IN PRODUCTION CODE */ final CountDownLatch latch = new CountDownLatch(1); - eventLoop.schedule(new Action0() { + eventLoop.submit(new Runnable() { @Override - public void call() { + public void run() { t = Thread.currentThread(); latch.countDown(); } @@ -184,11 +197,11 @@ public void call() { @Override public Worker createWorker() { - return eventLoop; + return single.createWorker(); } public void shutdown() { - s.unsubscribe(); + eventLoop.shutdownNow(); } public Thread getThread() { @@ -196,4 +209,4 @@ public Thread getThread() { } } -} +} \ No newline at end of file diff --git a/src/test/java/rx/internal/util/ScalarSynchronousObservableTest.java b/src/test/java/rx/internal/util/ScalarSynchronousObservableTest.java new file mode 100644 index 0000000000..fee7b6f8e1 --- /dev/null +++ b/src/test/java/rx/internal/util/ScalarSynchronousObservableTest.java @@ -0,0 +1,233 @@ +/** + * 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.util; + +import org.junit.Test; + +import rx.Observable; +import rx.exceptions.TestException; +import rx.functions.Func1; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; + +public class ScalarSynchronousObservableTest { + @Test + public void testBackpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void testBackpressureSubscribeOn() throws Exception { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).subscribeOn(Schedulers.computation()).subscribe(ts); + + Thread.sleep(200); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.awaitTerminalEvent(); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void testBackpressureObserveOn() throws Exception { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).observeOn(Schedulers.computation()).subscribe(ts); + + Thread.sleep(200); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.awaitTerminalEvent(); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void testBackpressureFlatMapJust() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.just(v); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void testBackpressureFlatMapRange() { + TestSubscriber ts = TestSubscriber.create(0); + + Observable.just(1).flatMap(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.assertValue(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + ts.assertValues(1, 2); + ts.assertCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValues(1, 2); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void emptiesAndJust() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.just(1) + .flatMap(new Func1>() { + @Override + public Observable call(Integer n) { + return Observable.just(null, null) + .filter(new Func1() { + @Override + public Boolean call(Object o) { + return o != null; + } + }) + .switchIfEmpty(Observable.empty().switchIfEmpty(Observable.just("Hello"))); + } + }).subscribe(ts); + + ts.assertValue("Hello"); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void syncObserverNextThrows() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + Observable.just(1).unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void syncFlatMapJustObserverNextThrows() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + Observable.just(1) + .flatMap(new Func1>() { + @Override + public Observable call(Integer v) { + return Observable.just(v); + } + }) + .unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test(timeout = 1000) + public void asyncObserverNextThrows() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + Observable.just(1).subscribeOn(Schedulers.computation()).unsafeSubscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } +} \ No newline at end of file From 3b932325391936309506db85aa08f4fdbbf9598d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 13 Jan 2016 23:47:58 +0100 Subject: [PATCH 482/857] 1.x: overhead reduction in range() and merge() operators --- .../internal/operators/OnSubscribeRange.java | 110 +++++++++--------- .../rx/internal/operators/OperatorMerge.java | 22 +++- 2 files changed, 76 insertions(+), 56 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeRange.java b/src/main/java/rx/internal/operators/OnSubscribeRange.java index c7631b2cb9..8f17303a2d 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeRange.java +++ b/src/main/java/rx/internal/operators/OnSubscribeRange.java @@ -25,108 +25,110 @@ */ public final class OnSubscribeRange implements OnSubscribe { - private final int start; - private final int end; + private final int startIndex; + private final int endIndex; public OnSubscribeRange(int start, int end) { - this.start = start; - this.end = end; + this.startIndex = start; + this.endIndex = end; } @Override - public void call(final Subscriber o) { - o.setProducer(new RangeProducer(o, start, end)); + public void call(final Subscriber childSubscriber) { + childSubscriber.setProducer(new RangeProducer(childSubscriber, startIndex, endIndex)); } private static final class RangeProducer extends AtomicLong implements Producer { /** */ private static final long serialVersionUID = 4114392207069098388L; - private final Subscriber o; - private final int end; - private long index; + private final Subscriber childSubscriber; + private final int endOfRange; + private long currentIndex; - RangeProducer(Subscriber o, int start, int end) { - this.o = o; - this.index = start; - this.end = end; + RangeProducer(Subscriber childSubscriber, int startIndex, int endIndex) { + this.childSubscriber = childSubscriber; + this.currentIndex = startIndex; + this.endOfRange = endIndex; } @Override - public void request(long n) { + public void request(long requestedAmount) { if (get() == Long.MAX_VALUE) { // already started with fast-path return; } - if (n == Long.MAX_VALUE && compareAndSet(0L, Long.MAX_VALUE)) { + if (requestedAmount == Long.MAX_VALUE && compareAndSet(0L, Long.MAX_VALUE)) { // fast-path without backpressure fastpath(); - } else if (n > 0L) { - long c = BackpressureUtils.getAndAddRequest(this, n); + } else if (requestedAmount > 0L) { + long c = BackpressureUtils.getAndAddRequest(this, requestedAmount); if (c == 0L) { // backpressure is requested - slowpath(n); + slowpath(requestedAmount); } } } /** - * + * Emits as many values as requested or remaining from the range, whichever is smaller. */ - void slowpath(long r) { - long idx = index; - while (true) { - /* - * This complicated logic is done to avoid touching the volatile `index` and `requested` values - * during the loop itself. If they are touched during the loop the performance is impacted significantly. - */ - long fs = end - idx + 1; - long e = Math.min(fs, r); - final boolean complete = fs <= r; - - fs = e + idx; - final Subscriber o = this.o; + void slowpath(long requestedAmount) { + long emitted = 0L; + long endIndex = endOfRange + 1L; + long index = currentIndex; + + final Subscriber childSubscriber = this.childSubscriber; + + for (;;) { - for (long i = idx; i != fs; i++) { - if (o.isUnsubscribed()) { + while (emitted != requestedAmount && index != endIndex) { + if (childSubscriber.isUnsubscribed()) { return; } - o.onNext((int) i); + + childSubscriber.onNext((int)index); + + index++; + emitted++; } - if (complete) { - if (o.isUnsubscribed()) { - return; - } - o.onCompleted(); + if (childSubscriber.isUnsubscribed()) { return; } - idx = fs; - index = fs; - - r = addAndGet(-e); - if (r == 0L) { - // we're done emitting the number requested so return + if (index == endIndex) { + childSubscriber.onCompleted(); return; } + + requestedAmount = get(); + + if (requestedAmount == emitted) { + currentIndex = index; + requestedAmount = addAndGet(-emitted); + if (requestedAmount == 0L) { + break; + } + emitted = 0L; + } } } /** - * + * Emits all remaining values without decrementing the requested amount. */ void fastpath() { - final long end = this.end + 1L; - final Subscriber o = this.o; - for (long i = index; i != end; i++) { - if (o.isUnsubscribed()) { + final long endIndex = this.endOfRange + 1L; + final Subscriber childSubscriber = this.childSubscriber; + for (long index = currentIndex; index != endIndex; index++) { + if (childSubscriber.isUnsubscribed()) { return; } - o.onNext((int) i); + childSubscriber.onNext((int) index); } - if (!o.isUnsubscribed()) { - o.onCompleted(); + if (!childSubscriber.isUnsubscribed()) { + childSubscriber.onCompleted(); } } } diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 56a7058d26..bb68edcbfe 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -177,6 +177,10 @@ static final class MergeSubscriber extends Subscriber /** An empty array to avoid creating new empty arrays in removeInner. */ static final InnerSubscriber[] EMPTY = new InnerSubscriber[0]; + final int scalarEmissionLimit; + + int scalarEmissionCount; + public MergeSubscriber(Subscriber child, boolean delayErrors, int maxConcurrent) { this.child = child; this.delayErrors = delayErrors; @@ -184,7 +188,13 @@ public MergeSubscriber(Subscriber child, boolean delayErrors, int max this.nl = NotificationLite.instance(); this.innerGuard = new Object(); this.innerSubscribers = EMPTY; - request(maxConcurrent == Integer.MAX_VALUE ? Long.MAX_VALUE : maxConcurrent); + if (maxConcurrent == Integer.MAX_VALUE) { + scalarEmissionLimit = Integer.MAX_VALUE; + request(Long.MAX_VALUE); + } else { + scalarEmissionLimit = Math.max(1, maxConcurrent >> 1); + request(maxConcurrent); + } } Queue getOrCreateErrorQueue() { @@ -488,7 +498,15 @@ protected void emitScalar(T value, long r) { if (r != Long.MAX_VALUE) { producer.produced(1); } - this.requestMore(1); + + int produced = scalarEmissionCount + 1; + if (produced == scalarEmissionLimit) { + scalarEmissionCount = 0; + this.requestMore(produced); + } else { + scalarEmissionCount = produced; + } + // check if some state changed while emitting synchronized (this) { skipFinal = true; From 25f7667feda66fa7711aece2b31d2122ef853f9b Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 20 Jan 2016 11:17:03 +1100 Subject: [PATCH 483/857] use Exceptions.throwOrError to simplify error handling --- src/main/java/rx/Single.java | 6 ++---- .../rx/internal/operators/OnSubscribeFromCallable.java | 3 +-- src/main/java/rx/internal/operators/OperatorScan.java | 9 +++------ src/main/java/rx/internal/operators/OperatorToMap.java | 6 ++---- .../java/rx/internal/operators/OperatorToMultimap.java | 6 ++---- src/main/java/rx/internal/operators/UnicastSubject.java | 3 +-- src/main/java/rx/observers/SafeSubscriber.java | 4 +--- src/main/java/rx/observers/SerializedObserver.java | 3 +-- 8 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 2d332e5de8..22a30438a8 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -185,17 +185,15 @@ public void call(Subscriber o) { st.onStart(); onSubscribe.call(st); } catch (Throwable e) { - Exceptions.throwIfFatal(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 - st.onError(e); + Exceptions.throwOrReport(e, st); } } 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); + Exceptions.throwOrReport(e, o); } } }); diff --git a/src/main/java/rx/internal/operators/OnSubscribeFromCallable.java b/src/main/java/rx/internal/operators/OnSubscribeFromCallable.java index 35eb62f04e..ed7f2183c4 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeFromCallable.java +++ b/src/main/java/rx/internal/operators/OnSubscribeFromCallable.java @@ -31,8 +31,7 @@ public void call(Subscriber subscriber) { try { singleDelayedProducer.setValue(resultFactory.call()); } catch (Throwable t) { - Exceptions.throwIfFatal(t); - subscriber.onError(t); + Exceptions.throwOrReport(t, subscriber); } } } diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index 4ce2e4ce4c..ccf7a74c07 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -108,8 +108,7 @@ public void onNext(T t) { try { v = accumulator.call(v, t); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - child.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, child, t); return; } } @@ -138,8 +137,7 @@ public void onNext(T currentValue) { try { v = accumulator.call(v, currentValue); } catch (Throwable e) { - Exceptions.throwIfFatal(e); - onError(OnErrorThrowable.addValueAsLastCause(e, currentValue)); + Exceptions.throwOrReport(e, this, currentValue); return; } value = v; @@ -322,8 +320,7 @@ void emitLoop() { try { child.onNext(v); } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - child.onError(OnErrorThrowable.addValueAsLastCause(ex, v)); + Exceptions.throwOrReport(ex, child, v); return; } r--; diff --git a/src/main/java/rx/internal/operators/OperatorToMap.java b/src/main/java/rx/internal/operators/OperatorToMap.java index 0a1ea9fcd0..0bd9e918a3 100644 --- a/src/main/java/rx/internal/operators/OperatorToMap.java +++ b/src/main/java/rx/internal/operators/OperatorToMap.java @@ -83,8 +83,7 @@ public Subscriber call(final Subscriber> subscriber try { localMap = mapFactory.call(); } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - subscriber.onError(ex); + Exceptions.throwOrReport(ex, subscriber); Subscriber parent = Subscribers.empty(); parent.unsubscribe(); return parent; @@ -110,8 +109,7 @@ public void onNext(T v) { key = keySelector.call(v); value = valueSelector.call(v); } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - subscriber.onError(ex); + Exceptions.throwOrReport(ex, subscriber); return; } diff --git a/src/main/java/rx/internal/operators/OperatorToMultimap.java b/src/main/java/rx/internal/operators/OperatorToMultimap.java index 1f3423e02c..6fee7620a9 100644 --- a/src/main/java/rx/internal/operators/OperatorToMultimap.java +++ b/src/main/java/rx/internal/operators/OperatorToMultimap.java @@ -138,8 +138,7 @@ public void onNext(T v) { key = keySelector.call(v); value = valueSelector.call(v); } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - subscriber.onError(ex); + Exceptions.throwOrReport(ex, subscriber); return; } @@ -148,8 +147,7 @@ public void onNext(T v) { try { collection = collectionFactory.call(key); } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - subscriber.onError(ex); + Exceptions.throwOrReport(ex, subscriber); return; } map.put(key, collection); diff --git a/src/main/java/rx/internal/operators/UnicastSubject.java b/src/main/java/rx/internal/operators/UnicastSubject.java index 44bba2b90e..5fb21b65f6 100644 --- a/src/main/java/rx/internal/operators/UnicastSubject.java +++ b/src/main/java/rx/internal/operators/UnicastSubject.java @@ -154,8 +154,7 @@ public void onNext(T t) { try { s.onNext(t); } catch (Throwable ex) { - Exceptions.throwIfFatal(ex); - s.onError(OnErrorThrowable.addValueAsLastCause(ex, t)); + Exceptions.throwOrReport(ex, s, t); } } } diff --git a/src/main/java/rx/observers/SafeSubscriber.java b/src/main/java/rx/observers/SafeSubscriber.java index 2baf0caaf9..e2df6ffe96 100644 --- a/src/main/java/rx/observers/SafeSubscriber.java +++ b/src/main/java/rx/observers/SafeSubscriber.java @@ -141,9 +141,7 @@ public void onNext(T args) { } catch (Throwable e) { // we handle here instead of another method so we don't add stacks to the frame // which can prevent it from being able to handle StackOverflow - Exceptions.throwIfFatal(e); - // handle errors if the onNext implementation fails, not just if the Observable fails - onError(e); + Exceptions.throwOrReport(e, this); } } diff --git a/src/main/java/rx/observers/SerializedObserver.java b/src/main/java/rx/observers/SerializedObserver.java index 8125ce54e6..ffb3670aac 100644 --- a/src/main/java/rx/observers/SerializedObserver.java +++ b/src/main/java/rx/observers/SerializedObserver.java @@ -95,8 +95,7 @@ public void onNext(T t) { actual.onNext(t); } catch (Throwable e) { terminated = true; - Exceptions.throwIfFatal(e); - actual.onError(OnErrorThrowable.addValueAsLastCause(e, t)); + Exceptions.throwOrReport(e, actual, t); return; } for (;;) { From cef0b916c546bf6178b493eafc1ea4adb0357e18 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 21 Jan 2016 11:10:21 +0100 Subject: [PATCH 484/857] 1.x: ConcatMapEager allow nulls from inner Observables. --- .../operators/OperatorEagerConcatMap.java | 19 +++++++++++-------- .../operators/OperatorEagerConcatMapTest.java | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java index 4df115b7ae..bbf2bcc48b 100644 --- a/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java +++ b/src/main/java/rx/internal/operators/OperatorEagerConcatMap.java @@ -166,6 +166,7 @@ void drain() { final AtomicLong requested = sharedProducer; final Subscriber actualSubscriber = this.actual; + final NotificationLite nl = NotificationLite.instance(); for (;;) { @@ -200,13 +201,13 @@ void drain() { long emittedAmount = 0L; boolean unbounded = requestedAmount == Long.MAX_VALUE; - Queue innerQueue = innerSubscriber.queue; + Queue innerQueue = innerSubscriber.queue; boolean innerDone = false; for (;;) { outerDone = innerSubscriber.done; - R v = innerQueue.peek(); + Object v = innerQueue.peek(); empty = v == null; if (outerDone) { @@ -237,7 +238,7 @@ void drain() { innerQueue.poll(); try { - actualSubscriber.onNext(v); + actualSubscriber.onNext(nl.getValue(v)); } catch (Throwable ex) { Exceptions.throwOrReport(ex, actualSubscriber, v); return; @@ -271,7 +272,8 @@ void drain() { static final class EagerInnerSubscriber extends Subscriber { final EagerOuterSubscriber parent; - final Queue queue; + final Queue queue; + final NotificationLite nl; volatile boolean done; Throwable error; @@ -279,19 +281,20 @@ static final class EagerInnerSubscriber extends Subscriber { public EagerInnerSubscriber(EagerOuterSubscriber parent, int bufferSize) { super(); this.parent = parent; - Queue q; + Queue q; if (UnsafeAccess.isUnsafeAvailable()) { - q = new SpscArrayQueue(bufferSize); + q = new SpscArrayQueue(bufferSize); } else { - q = new SpscAtomicArrayQueue(bufferSize); + q = new SpscAtomicArrayQueue(bufferSize); } this.queue = q; + this.nl = NotificationLite.instance(); request(bufferSize); } @Override public void onNext(T t) { - queue.offer(t); + queue.offer(nl.next(t)); parent.drain(); } diff --git a/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java b/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java index 8c7bd3d9e4..8d2d40bed4 100644 --- a/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java +++ b/src/test/java/rx/internal/operators/OperatorEagerConcatMapTest.java @@ -394,4 +394,20 @@ public void call(Integer t) { ts.assertNotCompleted(); Assert.assertEquals(RxRingBuffer.SIZE, count.get()); } + + @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); + } } From ca1dc2e1b1c796eefc35c115503029a2d10e361f Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 22 Jan 2016 19:33:30 +0100 Subject: [PATCH 485/857] 1.x: redo performance checker --- src/perf/java/rx/operators/RedoPerf.java | 116 +++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/perf/java/rx/operators/RedoPerf.java diff --git a/src/perf/java/rx/operators/RedoPerf.java b/src/perf/java/rx/operators/RedoPerf.java new file mode 100644 index 0000000000..2c5dc7f491 --- /dev/null +++ b/src/perf/java/rx/operators/RedoPerf.java @@ -0,0 +1,116 @@ +/** + * 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.Arrays; +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.internal.util.UtilityFunctions; +import rx.jmh.LatchedObserver; + +/** + * Benchmark typical atomic operations on volatile fields and AtomicXYZ classes. + *

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

    + * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*RedoPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class RedoPerf { + @Param({"1,1", "1,1000", "1,1000000", "1000,1", "1000,1000", "1000000,1"}) + public String params; + + public int len; + public int repeat; + + Observable sourceRepeating; + + Observable sourceRetrying; + + Observable redoRepeating; + + Observable redoRetrying; + + Observable baseline; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Setup + public void setup() { + String[] ps = params.split(","); + len = Integer.parseInt(ps[0]); + repeat = Integer.parseInt(ps[1]); + + Integer[] values = new Integer[len]; + Arrays.fill(values, 777); + + Observable source = Observable.from(values); + + Observable error = source.concatWith(Observable.error(new RuntimeException())); + + Integer[] values2 = new Integer[len * repeat]; + Arrays.fill(values2, 777); + + baseline = Observable.from(values2); + + sourceRepeating = source.repeat(repeat); + + sourceRetrying = error.retry(repeat); + + redoRepeating = source.repeatWhen((Func1)UtilityFunctions.identity()).take(len * repeat); + + redoRetrying = error.retryWhen((Func1)UtilityFunctions.identity()).take(len * repeat); + } + + @Benchmark + public void baseline(Blackhole bh) { + baseline.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void repeatCounted(Blackhole bh) { + sourceRepeating.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void retryCounted(Blackhole bh) { + sourceRetrying.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void repeatWhen(Blackhole bh) { + redoRepeating.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void retryWhen(Blackhole bh) { + redoRetrying.subscribe(new LatchedObserver(bh)); + } +} From 2ee019b1ff8a979982c8365b1d5796773170d1e9 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Fri, 22 Jan 2016 22:25:11 +0100 Subject: [PATCH 486/857] 1.x: zip performance measure --- build.gradle | 3 + src/perf/java/rx/operators/ZipPerf.java | 140 ++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 src/perf/java/rx/operators/ZipPerf.java diff --git a/build.gradle b/build.gradle index 8b934db6eb..20e8ddced1 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,9 @@ apply plugin: 'java' dependencies { testCompile 'junit:junit-dep:4.10' testCompile 'org.mockito:mockito-core:1.8.5' + + perfCompile 'org.openjdk.jmh:jmh-core:1.11.3' + perfCompile 'org.openjdk.jmh:jmh-generator-annprocess:1.11.3' } javadoc { diff --git a/src/perf/java/rx/operators/ZipPerf.java b/src/perf/java/rx/operators/ZipPerf.java new file mode 100644 index 0000000000..9ded231790 --- /dev/null +++ b/src/perf/java/rx/operators/ZipPerf.java @@ -0,0 +1,140 @@ +/** + * 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.Arrays; +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.Func2; +import rx.jmh.LatchedObserver; +import rx.schedulers.Schedulers; + +/** + * Benchmark the Zip operator. + *

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

    + * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*ZipPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class ZipPerf { + + @Param({"1", "1000", "1000000"}) + public int firstLen; + @Param({"1", "1000", "1000000"}) + public int secondLen; + + Observable baseline; + + Observable bothSync; + Observable firstSync; + Observable secondSync; + Observable bothAsync; + + boolean small; + + @Setup + public void setup() { + Integer[] array1 = new Integer[firstLen]; + Arrays.fill(array1, 777); + Integer[] array2 = new Integer[secondLen]; + Arrays.fill(array2, 777); + + baseline = Observable.from(firstLen < secondLen? array2 : array1); + + Observable o1 = Observable.from(array1); + + Observable o2 = Observable.from(array2); + + Func2 plus = new Func2() { + @Override + public Integer call(Integer a, Integer b) { + return a + b; + } + }; + + bothSync = Observable.zip(o1, o2, plus); + + firstSync = Observable.zip(o1, o2.subscribeOn(Schedulers.computation()), plus); + + secondSync = Observable.zip(o1.subscribeOn(Schedulers.computation()), o2, plus); + + bothAsync = Observable.zip(o1.subscribeOn(Schedulers.computation()), o2.subscribeOn(Schedulers.computation()), plus); + + small = Math.min(firstLen, secondLen) < 100; + } + + @Benchmark + public void baseline(Blackhole bh) { + baseline.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void syncSync(Blackhole bh) { + bothSync.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void syncAsync(Blackhole bh) throws Exception { + LatchedObserver o = new LatchedObserver(bh); + firstSync.subscribe(o); + + if (small) { + while (o.latch.getCount() != 0); + } else { + o.latch.await(); + } + } + + @Benchmark + public void asyncSync(Blackhole bh) throws Exception { + LatchedObserver o = new LatchedObserver(bh); + secondSync.subscribe(o); + + if (small) { + while (o.latch.getCount() != 0); + } else { + o.latch.await(); + } + } + + @Benchmark + public void asyncAsync(Blackhole bh) throws Exception { + LatchedObserver o = new LatchedObserver(bh); + bothAsync.subscribe(o); + + if (small) { + while (o.latch.getCount() != 0); + } else { + o.latch.await(); + } + } + +} From aaeadf713b1276b5d1f7fb2c976dbbb3dffc86b3 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 23 Jan 2016 17:43:05 +1100 Subject: [PATCH 487/857] handle predicate exceptions properly in skipWhile --- .../internal/operators/OperatorSkipWhile.java | 10 +++- .../operators/OperatorSkipWhileTest.java | 47 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorSkipWhile.java b/src/main/java/rx/internal/operators/OperatorSkipWhile.java index 7936901a0e..deea70afcd 100644 --- a/src/main/java/rx/internal/operators/OperatorSkipWhile.java +++ b/src/main/java/rx/internal/operators/OperatorSkipWhile.java @@ -17,6 +17,7 @@ import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.functions.Func1; import rx.functions.Func2; @@ -40,7 +41,14 @@ public void onNext(T t) { if (!skipping) { child.onNext(t); } else { - if (!predicate.call(t, index++)) { + final boolean skip; + try { + skip = predicate.call(t, index++); + } catch (Throwable e) { + Exceptions.throwOrReport(e, child, t); + return; + } + if (!skip) { skipping = false; child.onNext(t); } else { diff --git a/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java b/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java index 38a93bd5fb..d0d8f6960b 100644 --- a/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java +++ b/src/test/java/rx/internal/operators/OperatorSkipWhileTest.java @@ -15,6 +15,7 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertFalse; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.inOrder; @@ -23,12 +24,17 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.Test; import org.mockito.InOrder; import rx.Observable; import rx.Observer; +import rx.functions.Action1; import rx.functions.Func1; +import rx.observers.Subscribers; +import rx.observers.TestSubscriber; public class OperatorSkipWhileTest { @@ -51,6 +57,20 @@ public Boolean call(Integer value) { return index++ < 3; } }; + + private static final Func1 THROWS_NON_FATAL = new Func1() { + @Override + public Boolean call(Integer values) { + throw new RuntimeException(); + } + }; + + private static final Func1 THROWS_FATAL = new Func1() { + @Override + public Boolean call(Integer values) { + throw new OutOfMemoryError(); + } + }; @Test public void testSkipWithIndex() { @@ -120,6 +140,33 @@ public void testSkipError() { inOrder.verify(w, times(1)).onError(any(RuntimeException.class)); } + @Test + public void testPredicateRuntimeError() { + Observable.just(1).skipWhile(THROWS_NON_FATAL).subscribe(w); + InOrder inOrder = inOrder(w); + inOrder.verify(w, never()).onNext(anyInt()); + inOrder.verify(w, never()).onCompleted(); + inOrder.verify(w, times(1)).onError(any(RuntimeException.class)); + } + + @Test(expected = OutOfMemoryError.class) + public void testPredicateFatalError() { + Observable.just(1).skipWhile(THROWS_FATAL).unsafeSubscribe(Subscribers.empty()); + } + + @Test + public void testPredicateRuntimeErrorDoesNotGoUpstreamFirst() { + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + TestSubscriber ts = TestSubscriber.create(); + Observable.just(1).doOnError(new Action1() { + @Override + public void call(Throwable t) { + errorOccurred.set(true); + } + }).skipWhile(THROWS_NON_FATAL).subscribe(ts); + assertFalse(errorOccurred.get()); + } + @Test public void testSkipManySubscribers() { Observable src = Observable.range(1, 10).skipWhile(LESS_THAN_FIVE); From 25e7b2c85c1015b319c3b44d8625f25c69b1e5ec Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 23 Jan 2016 21:30:50 +1100 Subject: [PATCH 488/857] fix error handling in onBackpressureBuffer --- .../OperatorOnBackpressureBuffer.java | 11 +++++- .../OperatorOnBackpressureBufferTest.java | 36 +++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java index 4aff6fc162..9ab8f82869 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureBuffer.java @@ -22,6 +22,7 @@ import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; import rx.internal.util.BackpressureDrainManager; @@ -156,7 +157,15 @@ private boolean assertCapacity() { "Overflowed buffer of " + baseCapacity)); if (onOverflow != null) { - onOverflow.call(); + 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; + } } } return false; diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java index 004764dd0b..48fa099735 100644 --- a/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureBufferTest.java @@ -15,10 +15,17 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + 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.Observable.OnSubscribe; @@ -27,12 +34,10 @@ import rx.Subscription; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; +import rx.functions.Action1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - public class OperatorOnBackpressureBufferTest { @Test @@ -147,5 +152,30 @@ public void call(Subscriber s) { } }); + + private static final Action0 THROWS_NON_FATAL = new Action0() { + + @Override + public void call() { + throw new RuntimeException(); + }}; + + @Test + public void testNonFatalExceptionThrownByOnOverflowIsNotReportedByUpstream() { + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + TestSubscriber ts = TestSubscriber.create(0); + infinite + .subscribeOn(Schedulers.computation()) + .doOnError(new Action1() { + @Override + public void call(Throwable t) { + errorOccurred.set(true); + } + }) + .onBackpressureBuffer(1, THROWS_NON_FATAL) + .subscribe(ts); + ts.awaitTerminalEvent(); + assertFalse(errorOccurred.get()); + } } From b4a6fddb69cc00f35a568693381c9fc1caede952 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Thu, 17 Dec 2015 13:50:00 +0300 Subject: [PATCH 489/857] No more need to convert Singles to Observables for Single.zip() --- src/main/java/rx/Single.java | 168 ++++++++++++++++---------- src/test/java/rx/SingleTest.java | 201 +++++++++++++++++++++++++++++-- 2 files changed, 296 insertions(+), 73 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 60ef33b949..1630e69f32 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -956,9 +956,9 @@ public final static Observable merge(Single t1, Single{@code zip} does not operate by default on a particular {@link Scheduler}. * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -966,8 +966,13 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, final Func2 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2) }).lift(new OperatorZip(zipFunction)); + public static final Single zip(Single s1, Single s2, final Func2 zipFunction) { + 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,11 +985,11 @@ public final static Single zip(Single o1, Single{@code zip} does not operate by default on a particular {@link Scheduler}. * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single - * @param o3 + * @param s3 * a third source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -992,8 +997,13 @@ public final static Single zip(Single o1, SingleReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, Single o3, Func3 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3) }).lift(new OperatorZip(zipFunction)); + public static final Single zip(Single s1, Single s2, Single s3, final Func3 zipFunction) { + 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]); + } + }); } /** @@ -1006,13 +1016,13 @@ public final static Single zip(Single o1, Singl *

    {@code zip} does not operate by default on a particular {@link Scheduler}.
    * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single - * @param o3 + * @param s3 * a third source Single - * @param o4 + * @param s4 * a fourth source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -1020,8 +1030,13 @@ public final static Single zip(Single o1, Singl * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, Single o3, Single o4, Func4 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4) }).lift(new OperatorZip(zipFunction)); + public static final Single zip(Single s1, Single s2, Single s3, Single s4, final Func4 zipFunction) { + 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]); + } + }); } /** @@ -1034,15 +1049,15 @@ public final static Single zip(Single o1, S *
    {@code zip} does not operate by default on a particular {@link Scheduler}.
    * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single - * @param o3 + * @param s3 * a third source Single - * @param o4 + * @param s4 * a fourth source Single - * @param o5 + * @param s5 * a fifth source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -1050,8 +1065,13 @@ public final static Single zip(Single o1, S * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Func5 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5) }).lift(new OperatorZip(zipFunction)); + public static final 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() { + @Override + public R call(Object... args) { + return zipFunction.call((T1) args[0], (T2) args[1], (T3) args[2], (T4) args[3], (T5) args[4]); + } + }); } /** @@ -1064,17 +1084,17 @@ public final static Single zip(Single o *
    {@code zip} does not operate by default on a particular {@link Scheduler}.
    * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single - * @param o3 + * @param s3 * a third source Single - * @param o4 + * @param s4 * a fourth source Single - * @param o5 + * @param s5 * a fifth source Single - * @param o6 + * @param s6 * a sixth source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -1082,9 +1102,14 @@ public final static Single zip(Single o * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, - Func6 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6) }).lift(new OperatorZip(zipFunction)); + public static final 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() { + @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]); + } + }); } /** @@ -1097,19 +1122,19 @@ public final static Single zip(Single{@code zip} does not operate by default on a particular {@link Scheduler}. * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single - * @param o3 + * @param s3 * a third source Single - * @param o4 + * @param s4 * a fourth source Single - * @param o5 + * @param s5 * a fifth source Single - * @param o6 + * @param s6 * a sixth source Single - * @param o7 + * @param s7 * a seventh source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -1117,9 +1142,14 @@ public final static Single zip(SingleReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, - Func7 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6), asObservable(o7) }).lift(new OperatorZip(zipFunction)); + public static final 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() { + @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]); + } + }); } /** @@ -1132,21 +1162,21 @@ public final static Single zip(Single{@code zip} does not operate by default on a particular {@link Scheduler}. * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single - * @param o3 + * @param s3 * a third source Single - * @param o4 + * @param s4 * a fourth source Single - * @param o5 + * @param s5 * a fifth source Single - * @param o6 + * @param s6 * a sixth source Single - * @param o7 + * @param s7 * a seventh source Single - * @param o8 + * @param s8 * an eighth source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -1154,9 +1184,14 @@ public final static Single zip(SingleReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, - Func8 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6), asObservable(o7), asObservable(o8) }).lift(new OperatorZip(zipFunction)); + public static final 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() { + @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]); + } + }); } /** @@ -1169,23 +1204,23 @@ public final static Single zip(Single{@code zip} does not operate by default on a particular {@link Scheduler}. * * - * @param o1 + * @param s1 * the first source Single - * @param o2 + * @param s2 * a second source Single - * @param o3 + * @param s3 * a third source Single - * @param o4 + * @param s4 * a fourth source Single - * @param o5 + * @param s5 * a fifth source Single - * @param o6 + * @param s6 * a sixth source Single - * @param o7 + * @param s7 * a seventh source Single - * @param o8 + * @param s8 * an eighth source Single - * @param o9 + * @param s9 * a ninth source Single * @param zipFunction * a function that, when applied to the item emitted by each of the source Singles, results in an @@ -1193,9 +1228,14 @@ public final static Single zip(SingleReactiveX operators documentation: Zip */ - public final static Single zip(Single o1, Single o2, Single o3, Single o4, Single o5, Single o6, Single o7, Single o8, - Single o9, Func9 zipFunction) { - return just(new Observable[] { asObservable(o1), asObservable(o2), asObservable(o3), asObservable(o4), asObservable(o5), asObservable(o6), asObservable(o7), asObservable(o8), asObservable(o9) }).lift(new OperatorZip(zipFunction)); + public static final 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() { + @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]); + } + }); } /** diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 2871450708..b29fcb01af 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -27,7 +27,6 @@ import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; @@ -45,11 +44,17 @@ import org.mockito.stubbing.Answer; import rx.Single.OnSubscribe; import rx.exceptions.CompositeException; -import rx.functions.Action; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; +import rx.functions.Func3; +import rx.functions.Func4; +import rx.functions.Func5; +import rx.functions.Func6; +import rx.functions.Func7; +import rx.functions.Func8; +import rx.functions.Func9; import rx.functions.FuncN; import rx.schedulers.TestScheduler; import rx.singles.BlockingSingle; @@ -102,21 +107,199 @@ public String call(String s) { } @Test - public void testZip() { + public void zip2Singles() { TestSubscriber ts = new TestSubscriber(); - Single a = Single.just("A"); - Single b = Single.just("B"); + Single a = Single.just(1); + Single b = Single.just(2); - Single.zip(a, b, new Func2() { + Single.zip(a, b, new Func2() { @Override - public String call(String a, String b) { - return a + b; + public String call(Integer a, Integer b) { + return "" + a + b; } }) .subscribe(ts); - ts.assertReceivedOnNext(Arrays.asList("AB")); + + ts.assertValue("12"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void zip3Singles() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just(1); + Single b = Single.just(2); + Single c = Single.just(3); + + Single.zip(a, b, c, new Func3() { + + @Override + public String call(Integer a, Integer b, Integer c) { + return "" + a + b + c; + } + + }) + .subscribe(ts); + + ts.assertValue("123"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void zip4Singles() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just(1); + Single b = Single.just(2); + Single c = Single.just(3); + Single d = Single.just(4); + + Single.zip(a, b, c, d, new Func4() { + + @Override + public String call(Integer a, Integer b, Integer c, Integer d) { + return "" + a + b + c + d; + } + + }) + .subscribe(ts); + + ts.assertValue("1234"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void zip5Singles() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just(1); + Single b = Single.just(2); + Single c = Single.just(3); + Single d = Single.just(4); + Single e = Single.just(5); + + Single.zip(a, b, c, d, e, new Func5() { + + @Override + public String call(Integer a, Integer b, Integer c, Integer d, Integer e) { + return "" + a + b + c + d + e; + } + + }) + .subscribe(ts); + + ts.assertValue("12345"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void zip6Singles() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just(1); + Single b = Single.just(2); + Single c = Single.just(3); + Single d = Single.just(4); + Single e = Single.just(5); + Single f = Single.just(6); + + Single.zip(a, b, c, d, e, f, new Func6() { + + @Override + public String call(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f) { + return "" + a + b + c + d + e + f; + } + + }) + .subscribe(ts); + + ts.assertValue("123456"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void zip7Singles() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just(1); + Single b = Single.just(2); + Single c = Single.just(3); + Single d = Single.just(4); + Single e = Single.just(5); + Single f = Single.just(6); + Single g = Single.just(7); + + Single.zip(a, b, c, d, e, f, g, new Func7() { + + @Override + public String call(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g) { + return "" + a + b + c + d + e + f + g; + } + + }) + .subscribe(ts); + + ts.assertValue("1234567"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void zip8Singles() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just(1); + Single b = Single.just(2); + Single c = Single.just(3); + Single d = Single.just(4); + Single e = Single.just(5); + Single f = Single.just(6); + Single g = Single.just(7); + Single h = Single.just(8); + + Single.zip(a, b, c, d, e, f, g, h, new Func8() { + + @Override + public String call(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, Integer h) { + return "" + a + b + c + d + e + f + g + h; + } + + }) + .subscribe(ts); + + ts.assertValue("12345678"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void zip9Singles() { + TestSubscriber ts = new TestSubscriber(); + Single a = Single.just(1); + Single b = Single.just(2); + Single c = Single.just(3); + Single d = Single.just(4); + Single e = Single.just(5); + Single f = Single.just(6); + Single g = Single.just(7); + Single h = Single.just(8); + Single i = Single.just(9); + + Single.zip(a, b, c, d, e, f, g, h, i, new Func9() { + + @Override + public String call(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, Integer h, Integer i) { + return "" + a + b + c + d + e + f + g + h + i; + } + + }) + .subscribe(ts); + + ts.assertValue("123456789"); + ts.assertCompleted(); + ts.assertNoErrors(); } @Test From 96751896fc33348212d8ed84ea6019a08b346eb1 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sun, 24 Jan 2016 10:10:29 +1100 Subject: [PATCH 490/857] fix onBackpressureDrop error handling of failure in onDrop action --- .../operators/OperatorOnBackpressureDrop.java | 8 ++++- .../OperatorOnBackpressureDropTest.java | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java index a9a8def2d4..dee334bb4d 100644 --- a/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java +++ b/src/main/java/rx/internal/operators/OperatorOnBackpressureDrop.java @@ -20,6 +20,7 @@ import rx.Observable.Operator; import rx.Producer; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.functions.Action1; public class OperatorOnBackpressureDrop implements Operator { @@ -84,7 +85,12 @@ public void onNext(T t) { } else { // item dropped if(onDrop != null) { - onDrop.call(t); + try { + onDrop.call(t); + } catch (Throwable e) { + Exceptions.throwOrReport(e, child, t); + return; + } } } } diff --git a/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java b/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java index b61f000704..1489e0c5ae 100644 --- a/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnBackpressureDropTest.java @@ -16,8 +16,10 @@ package rx.internal.operators; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; @@ -26,6 +28,8 @@ import rx.Observable.OnSubscribe; import rx.Observer; import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -117,6 +121,33 @@ public void onNext(Long t) { }}); assertEquals(n, count.get()); } + + @Test + public void testNonFatalExceptionFromOverflowActionIsNotReportedFromUpstreamOperator() { + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + //request 0 + TestSubscriber ts = TestSubscriber.create(0); + //range method emits regardless of requests so should trigger onBackpressureDrop action + range(2) + // if haven't caught exception in onBackpressureDrop operator then would incorrectly + // be picked up by this call to doOnError + .doOnError(new Action1() { + @Override + public void call(Throwable t) { + errorOccurred.set(true); + } + }) + .onBackpressureDrop(THROW_NON_FATAL) + .subscribe(ts); + assertFalse(errorOccurred.get()); + } + + private static final Action1 THROW_NON_FATAL = new Action1() { + @Override + public void call(Long n) { + throw new RuntimeException(); + } + }; static final Observable infinite = Observable.create(new OnSubscribe() { From 300ab1ea198f1a5f7734063dda7acdf4efc8d450 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Sat, 23 Jan 2016 18:10:28 +1100 Subject: [PATCH 491/857] fix error handling in OperatorDistinctUntilChanged --- .../OperatorDistinctUntilChanged.java | 9 +++++- .../OperatorDistinctUntilChangedTest.java | 32 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java b/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java index 275e33d0db..0d98b3248f 100644 --- a/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java +++ b/src/main/java/rx/internal/operators/OperatorDistinctUntilChanged.java @@ -17,6 +17,7 @@ import rx.Observable.Operator; import rx.Subscriber; +import rx.exceptions.Exceptions; import rx.functions.Func1; import rx.internal.util.UtilityFunctions; @@ -56,7 +57,13 @@ public Subscriber call(final Subscriber child) { @Override public void onNext(T t) { U currentKey = previousKey; - U key = keySelector.call(t); + final U key; + try { + key = keySelector.call(t); + } catch (Throwable e) { + Exceptions.throwOrReport(e, child, t); + return; + } previousKey = key; if (hasPrevious) { diff --git a/src/test/java/rx/internal/operators/OperatorDistinctUntilChangedTest.java b/src/test/java/rx/internal/operators/OperatorDistinctUntilChangedTest.java index fc81a6a906..a913345026 100644 --- a/src/test/java/rx/internal/operators/OperatorDistinctUntilChangedTest.java +++ b/src/test/java/rx/internal/operators/OperatorDistinctUntilChangedTest.java @@ -15,6 +15,7 @@ */ package rx.internal.operators; +import static org.junit.Assert.assertFalse; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.inOrder; @@ -23,6 +24,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; import org.junit.Test; @@ -31,17 +33,18 @@ import rx.Observable; import rx.Observer; +import rx.functions.Action1; import rx.functions.Func1; public class OperatorDistinctUntilChangedTest { @Mock - Observer w; + private Observer w; @Mock - Observer w2; + private Observer w2; // nulls lead to exceptions - final Func1 TO_UPPER_WITH_EXCEPTION = new Func1() { + private final static Func1 TO_UPPER_WITH_EXCEPTION = new Func1() { @Override public String call(String s) { if (s.equals("x")) { @@ -50,6 +53,13 @@ public String call(String s) { return s.toUpperCase(); } }; + + private final static Func1 THROWS_NON_FATAL = new Func1() { + @Override + public String call(String s) { + throw new RuntimeException(); + } + }; @Before public void before() { @@ -138,4 +148,20 @@ public void testDistinctUntilChangedOfSourceWithExceptionsFromKeySelector() { inOrder.verify(w, never()).onNext(anyString()); inOrder.verify(w, never()).onCompleted(); } + + @Test + public void testDistinctUntilChangedWhenNonFatalExceptionThrownByKeySelectorIsNotReportedByUpstream() { + Observable src = Observable.just("a", "b", null, "c"); + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + src + .doOnError(new Action1() { + @Override + public void call(Throwable t) { + errorOccurred.set(true); + } + }) + .distinctUntilChanged(THROWS_NON_FATAL) + .subscribe(w); + assertFalse(errorOccurred.get()); + } } From 8eee47660b1aef7204ee809bd2e378a1aaf2825d Mon Sep 17 00:00:00 2001 From: akarnokd Date: Mon, 25 Jan 2016 10:30:04 +0100 Subject: [PATCH 492/857] 1.x: Single performance measurements --- src/perf/java/rx/SingleSourcePerf.java | 263 +++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 src/perf/java/rx/SingleSourcePerf.java diff --git a/src/perf/java/rx/SingleSourcePerf.java b/src/perf/java/rx/SingleSourcePerf.java new file mode 100644 index 0000000000..fff9006ea6 --- /dev/null +++ b/src/perf/java/rx/SingleSourcePerf.java @@ -0,0 +1,263 @@ +/** + * 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; + +import java.util.concurrent.*; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import rx.functions.Func1; +import rx.schedulers.Schedulers; + +/** + * Benchmark Single. + *

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

    + * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*SingleSourcePerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class SingleSourcePerf { + + Single source; + + Single flatmapped; + + Single flatmappedConst; + + Single sourceObserveOn; + + Single sourceSubscribeOn; + + Single sourceObserveOnExecutor; + + Single sourceSubscribeOnExecutor; + + Single sourceObserveOnScheduledExecutor; + + Single sourceSubscribeOnScheduledExecutor; + +// Single sourceObserveOnFJ; + +// Single sourceSubscribeOnFJ; + + ScheduledExecutorService scheduledExecutor; + + ExecutorService executor; + + @Setup + public void setup() { + source = Single.just(1); + + flatmapped = source.flatMap(new Func1>() { + @Override + public Single call(Integer t) { + return Single.just(t); + } + }); + + flatmapped = source.flatMap(new Func1>() { + @Override + public Single call(Integer t) { + return source; + } + }); + + sourceObserveOn = source.observeOn(Schedulers.computation()); + + sourceSubscribeOn = source.subscribeOn(Schedulers.computation()); + + // ---------- + + scheduledExecutor = Executors.newScheduledThreadPool(1); + + Scheduler s = Schedulers.from(scheduledExecutor); + + sourceObserveOnScheduledExecutor = source.observeOn(s); + + sourceSubscribeOnScheduledExecutor = source.subscribeOn(s); + + // ---------- + + executor = Executors.newSingleThreadExecutor(); + + Scheduler se = Schedulers.from(executor); + + sourceObserveOnExecutor = source.observeOn(se); + + sourceSubscribeOnExecutor = source.subscribeOn(se); + + // -------- + +// Scheduler fj = Schedulers.from(ForkJoinPool.commonPool()); + +// sourceObserveOnFJ = source.observeOn(fj); + +// sourceSubscribeOnFJ = source.subscribeOn(fj); + } + + @TearDown + public void teardown() { + scheduledExecutor.shutdownNow(); + + executor.shutdownNow(); + } + + static final class PlainSingleSubscriber extends SingleSubscriber { + final Blackhole bh; + + public PlainSingleSubscriber(Blackhole bh) { + this.bh = bh; + } + + @Override + public void onSuccess(Object value) { + bh.consume(value); + } + + @Override + public void onError(Throwable error) { + bh.consume(error); + } + } + + static final class LatchedSingleSubscriber extends SingleSubscriber { + final Blackhole bh; + + final CountDownLatch cdl; + + public LatchedSingleSubscriber(Blackhole bh) { + this.bh = bh; + this.cdl = new CountDownLatch(1); + } + + @Override + public void onSuccess(Object value) { + bh.consume(value); + cdl.countDown(); + } + + @Override + public void onError(Throwable error) { + bh.consume(error); + cdl.countDown(); + } + + public void await() { + try { + cdl.await(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + public void awaitSpin() { + while (cdl.getCount() != 0L) ; + } + } + + @Benchmark + public void direct(Blackhole bh) { + source.subscribe(new PlainSingleSubscriber(bh)); + } + + @Benchmark + public void flatmap(Blackhole bh) { + flatmapped.subscribe(new PlainSingleSubscriber(bh)); + } + + @Benchmark + public void flatmapConst(Blackhole bh) { + flatmapped.subscribe(new PlainSingleSubscriber(bh)); + } + + @Benchmark + public void observeOn(Blackhole bh) { + LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); + + sourceObserveOn.subscribe(o); + + o.awaitSpin(); + } + + @Benchmark + public void observeOnExec(Blackhole bh) { + LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); + + sourceObserveOnExecutor.subscribe(o); + + o.awaitSpin(); + } + + @Benchmark + public void subscribeOn(Blackhole bh) { + LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); + + sourceSubscribeOn.subscribe(o); + + o.awaitSpin(); + } + + @Benchmark + public void subscribeOnExec(Blackhole bh) { + LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); + + sourceSubscribeOnExecutor.subscribe(o); + + o.awaitSpin(); + } + + @Benchmark + public void subscribeOnSchExec(Blackhole bh) { + LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); + + sourceSubscribeOnScheduledExecutor.subscribe(o); + + o.awaitSpin(); + } + +// @Benchmark +// public void subscribeOnFJ(Blackhole bh) { +// LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); +// +// sourceSubscribeOnFJ.subscribe(o); +// +// o.awaitSpin(); +// } + + @Benchmark + public void observeOnSchExec(Blackhole bh) { + LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); + + sourceObserveOnScheduledExecutor.subscribe(o); + + o.awaitSpin(); + } + +// @Benchmark +// public void observeOnFJ(Blackhole bh) { +// LatchedSingleSubscriber o = new LatchedSingleSubscriber(bh); +// +// sourceObserveOnFJ.subscribe(o); +// +// o.awaitSpin(); +// } + +} From 05d8c63b9771c20279b9ee36357e0e300fe12b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 25 Jan 2016 22:17:43 +0100 Subject: [PATCH 493/857] 1.x: fix SyncOnSubscribe not signalling onError if the generator crashes --- .../java/rx/observables/SyncOnSubscribe.java | 19 +++++++++++++-- .../rx/observables/SyncOnSubscribeTest.java | 23 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/observables/SyncOnSubscribe.java b/src/main/java/rx/observables/SyncOnSubscribe.java index 707e047b2a..f8cda8dde0 100644 --- a/src/main/java/rx/observables/SyncOnSubscribe.java +++ b/src/main/java/rx/observables/SyncOnSubscribe.java @@ -24,6 +24,7 @@ import rx.Subscriber; import rx.Subscription; import rx.annotations.Experimental; +import rx.exceptions.Exceptions; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Action2; @@ -53,7 +54,16 @@ public abstract class SyncOnSubscribe implements OnSubscribe { */ @Override public final void call(final Subscriber subscriber) { - S state = generateState(); + S state; + + try { + state = generateState(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + subscriber.onError(e); + return; + } + SubscriptionProducer p = new SubscriptionProducer(subscriber, this, state); subscriber.add(p); subscriber.setProducer(p); @@ -363,7 +373,12 @@ private boolean tryUnsubscribe() { } private void doUnsubscribe() { - parent.onUnsubscribe(state); + try { + parent.onUnsubscribe(state); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); + } } @Override diff --git a/src/test/java/rx/observables/SyncOnSubscribeTest.java b/src/test/java/rx/observables/SyncOnSubscribeTest.java index 82cfc0b033..71fc0ac8e9 100644 --- a/src/test/java/rx/observables/SyncOnSubscribeTest.java +++ b/src/test/java/rx/observables/SyncOnSubscribeTest.java @@ -989,4 +989,27 @@ public Object call() throws Exception { if (exec != null) exec.shutdownNow(); } } + + @Test + public void testStateThrows() { + TestSubscriber ts = new TestSubscriber(); + + SyncOnSubscribe.createSingleState( + new Func0() { + @Override + public Object call() { + throw new TestException(); + } + } + , new Action2>() { + @Override + public void call(Object s, Observer o) { + + } + }).call(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } } From 0f8caf334b4f2ae077ba60ccdbfb650821600fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 25 Jan 2016 22:58:44 +0100 Subject: [PATCH 494/857] 1.x: fix Amb sharing the choice among all subscribers --- .../rx/internal/operators/OnSubscribeAmb.java | 4 ++-- .../operators/OnSubscribeAmbTest.java | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/internal/operators/OnSubscribeAmb.java b/src/main/java/rx/internal/operators/OnSubscribeAmb.java index 2fe48d812f..2ba94b0f97 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeAmb.java +++ b/src/main/java/rx/internal/operators/OnSubscribeAmb.java @@ -353,8 +353,6 @@ public void unsubscribeOthers(AmbSubscriber notThis) { //give default access instead of private as a micro-optimization //for access from anonymous classes below final Iterable> sources; - final Selection selection = new Selection(); - final AtomicReference> choice = selection.choice; private OnSubscribeAmb(Iterable> sources) { this.sources = sources; @@ -362,6 +360,8 @@ private OnSubscribeAmb(Iterable> sources) { @Override public void call(final Subscriber subscriber) { + final Selection selection = new Selection(); + final AtomicReference> choice = selection.choice; //setup unsubscription of all the subscribers to the sources subscriber.add(Subscriptions.create(new Action0() { diff --git a/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java b/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java index 76cb40800e..4173c56eb5 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeAmbTest.java @@ -288,5 +288,26 @@ public void call(Object t) { }).ambWith(Observable.just(2)).toBlocking().single(); assertEquals(1, result); } - + + @Test(timeout = 1000) + public void testMultipleUse() { + TestSubscriber ts1 = new TestSubscriber(); + TestSubscriber ts2 = new TestSubscriber(); + + Observable amb = Observable.timer(100, TimeUnit.MILLISECONDS).ambWith(Observable.timer(200, TimeUnit.MILLISECONDS)); + + amb.subscribe(ts1); + amb.subscribe(ts2); + + ts1.awaitTerminalEvent(); + ts2.awaitTerminalEvent(); + + ts1.assertValue(0L); + ts1.assertCompleted(); + ts1.assertNoErrors(); + + ts2.assertValue(0L); + ts2.assertCompleted(); + ts2.assertNoErrors(); + } } From 9cf3754033da4e24758420ca5acd25fb04a31125 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Wed, 27 Jan 2016 03:52:48 +0300 Subject: [PATCH 495/857] Remove redundant "final" modifiers --- src/main/java/rx/Observable.java | 194 +++++++++--------- src/main/java/rx/Single.java | 66 +++--- .../rx/exceptions/CompositeException.java | 2 +- .../rx/internal/operators/OnSubscribeAmb.java | 2 +- .../rx/internal/util/IndexedRingBuffer.java | 2 +- .../util/ScalarSynchronousObservable.java | 2 +- .../util/atomic/SpscLinkedArrayQueue.java | 8 +- .../atomic/SpscUnboundedAtomicArrayQueue.java | 8 +- .../util/unsafe/SpscUnboundedArrayQueue.java | 8 +- .../rx/internal/util/unsafe/UnsafeAccess.java | 2 +- .../rx/observables/GroupedObservable.java | 2 +- src/main/java/rx/observers/Observers.java | 6 +- src/main/java/rx/observers/Subscribers.java | 6 +- src/main/java/rx/subjects/ReplaySubject.java | 2 +- 14 files changed, 155 insertions(+), 155 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 8e94b3fc0d..7754f87c43 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -91,7 +91,7 @@ protected Observable(OnSubscribe f) { * function * @see ReactiveX operators documentation: Create */ - public final static Observable create(OnSubscribe f) { + public static Observable create(OnSubscribe f) { return new Observable(hook.onCreate(f)); } @@ -279,7 +279,7 @@ public Completable toCompletable() { * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Iterable> sources) { + public static Observable amb(Iterable> sources) { return create(OnSubscribeAmb.amb(sources)); } @@ -301,7 +301,7 @@ public final static Observable amb(IterableReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2) { + public static Observable amb(Observable o1, Observable o2) { return create(OnSubscribeAmb.amb(o1, o2)); } @@ -325,7 +325,7 @@ public final static Observable amb(Observable o1, Observable * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2, Observable o3) { + public static Observable amb(Observable o1, Observable o2, Observable o3) { return create(OnSubscribeAmb.amb(o1, o2, o3)); } @@ -351,7 +351,7 @@ public final static Observable amb(Observable o1, Observable * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4) { + public static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4)); } @@ -379,7 +379,7 @@ public final static Observable amb(Observable o1, Observable * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5) { + public static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5)); } @@ -409,7 +409,7 @@ public final static Observable amb(Observable o1, Observable * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6) { + public static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6)); } @@ -441,7 +441,7 @@ public final static Observable amb(Observable o1, Observable * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7) { + public static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6, o7)); } @@ -475,7 +475,7 @@ public final static Observable amb(Observable o1, Observable * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8) { + public static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8)); } @@ -511,7 +511,7 @@ public final static Observable amb(Observable o1, Observable * emitted an item or sent a termination notification * @see ReactiveX operators documentation: Amb */ - public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9) { + public static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9) { return create(OnSubscribeAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8, o9)); } @@ -537,7 +537,7 @@ public final static Observable amb(Observable o1, Observable * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Func2 combineFunction) { + public static Observable combineLatest(Observable o1, Observable o2, Func2 combineFunction) { return combineLatest(Arrays.asList(o1, o2), Functions.fromFunc(combineFunction)); } @@ -565,7 +565,7 @@ public static final Observable combineLatest(ObservableReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Func3 combineFunction) { + public static Observable combineLatest(Observable o1, Observable o2, Observable o3, Func3 combineFunction) { return combineLatest(Arrays.asList(o1, o2, o3), Functions.fromFunc(combineFunction)); } @@ -595,7 +595,7 @@ public static final Observable combineLatest(ObservableReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, + public static Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Func4 combineFunction) { return combineLatest(Arrays.asList(o1, o2, o3, o4), Functions.fromFunc(combineFunction)); } @@ -628,7 +628,7 @@ public static final Observable combineLatest(ObservableReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, + public static 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)); } @@ -663,7 +663,7 @@ public static final Observable combineLatest(Observab * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, + public static 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)); } @@ -700,7 +700,7 @@ public static final Observable combineLatest(Obse * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, + 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)); } @@ -739,7 +739,7 @@ public static final Observable combineLatest( * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, + 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)); } @@ -780,7 +780,7 @@ public static final Observable combineLat * @see ReactiveX operators documentation: CombineLatest */ @SuppressWarnings("unchecked") - public static final Observable combineLatest(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, + 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)); @@ -806,7 +806,7 @@ public static final Observable combin * Observables by means of the given aggregation function * @see ReactiveX operators documentation: CombineLatest */ - public static final Observable combineLatest(List> sources, FuncN combineFunction) { + public static Observable combineLatest(List> sources, FuncN combineFunction) { return create(new OnSubscribeCombineLatest(sources, combineFunction)); } @@ -826,7 +826,7 @@ public static final Observable combineLatest(ListReactiveX operators documentation: Concat */ - public final static Observable concat(Observable> observables) { + public static Observable concat(Observable> observables) { return observables.lift(OperatorConcat.instance()); } @@ -848,7 +848,7 @@ public final static Observable concat(ObservableReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2) { + public static Observable concat(Observable t1, Observable t2) { return concat(just(t1, t2)); } @@ -872,7 +872,7 @@ public final static Observable concat(Observable t1, Observa * without interleaving them * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2, Observable t3) { + public static Observable concat(Observable t1, Observable t2, Observable t3) { return concat(just(t1, t2, t3)); } @@ -898,7 +898,7 @@ public final static Observable concat(Observable t1, Observa * without interleaving them * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4) { + public static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4) { return concat(just(t1, t2, t3, t4)); } @@ -926,7 +926,7 @@ public final static Observable concat(Observable t1, Observa * without interleaving them * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { + public static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { return concat(just(t1, t2, t3, t4, t5)); } @@ -956,7 +956,7 @@ public final static Observable concat(Observable t1, Observa * without interleaving them * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { + public static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { return concat(just(t1, t2, t3, t4, t5, t6)); } @@ -988,7 +988,7 @@ public final static Observable concat(Observable t1, Observa * without interleaving them * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { + public static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { return concat(just(t1, t2, t3, t4, t5, t6, t7)); } @@ -1022,7 +1022,7 @@ public final static Observable concat(Observable t1, Observa * without interleaving them * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { + public static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { return concat(just(t1, t2, t3, t4, t5, t6, t7, t8)); } @@ -1058,7 +1058,7 @@ public final static Observable concat(Observable t1, Observa * without interleaving them * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { + public static Observable concat(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { return concat(just(t1, t2, t3, t4, t5, t6, t7, t8, t9)); } @@ -1086,7 +1086,7 @@ public final static Observable concat(Observable t1, Observa * Observable factory function * @see ReactiveX operators documentation: Defer */ - public final static Observable defer(Func0> observableFactory) { + public static Observable defer(Func0> observableFactory) { return create(new OnSubscribeDefer(observableFactory)); } @@ -1117,7 +1117,7 @@ public void call(Subscriber subscriber) { * @see ReactiveX operators documentation: Empty */ @SuppressWarnings("unchecked") - public final static Observable empty() { + public static Observable empty() { return (Observable) EmptyHolder.INSTANCE; } @@ -1139,7 +1139,7 @@ public final static Observable empty() { * the Observer subscribes to it * @see ReactiveX operators documentation: Throw */ - public final static Observable error(Throwable exception) { + public static Observable error(Throwable exception) { return new ThrowObservable(exception); } @@ -1166,7 +1166,7 @@ public final static Observable error(Throwable exception) { * @return an Observable that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ - public final static Observable from(Future future) { + public static Observable from(Future future) { return create(OnSubscribeToObservableFuture.toObservableFuture(future)); } @@ -1197,7 +1197,7 @@ public final static Observable from(Future future) { * @return an Observable that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ - public final static Observable from(Future future, long timeout, TimeUnit unit) { + public static Observable from(Future future, long timeout, TimeUnit unit) { return create(OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); } @@ -1225,7 +1225,7 @@ public final static Observable from(Future future, long time * @return an Observable that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ - public final static Observable from(Future future, Scheduler scheduler) { + 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); } @@ -1247,7 +1247,7 @@ public final static Observable from(Future future, Scheduler * @return an Observable that emits each item in the source {@link Iterable} sequence * @see ReactiveX operators documentation: From */ - public final static Observable from(Iterable iterable) { + public static Observable from(Iterable iterable) { return create(new OnSubscribeFromIterable(iterable)); } @@ -1267,7 +1267,7 @@ public final static Observable from(Iterable iterable) { * @return an Observable that emits each item in the source Array * @see ReactiveX operators documentation: From */ - public final static Observable from(T[] array) { + public static Observable from(T[] array) { int n = array.length; if (n == 0) { return empty(); @@ -1321,7 +1321,7 @@ public static Observable fromCallable(Callable func) { * @return an Observable that emits a sequential number each time interval * @see ReactiveX operators documentation: Interval */ - public final static Observable interval(long interval, TimeUnit unit) { + public static Observable interval(long interval, TimeUnit unit) { return interval(interval, interval, unit, Schedulers.computation()); } @@ -1344,7 +1344,7 @@ public final static Observable interval(long interval, TimeUnit unit) { * @return an Observable that emits a sequential number each time interval * @see ReactiveX operators documentation: Interval */ - public final static Observable interval(long interval, TimeUnit unit, Scheduler scheduler) { + public static Observable interval(long interval, TimeUnit unit, Scheduler scheduler) { return interval(interval, interval, unit, scheduler); } @@ -1372,7 +1372,7 @@ public final static Observable interval(long interval, TimeUnit unit, Sche * @see ReactiveX operators documentation: Interval * @since 1.0.12 */ - public final static Observable interval(long initialDelay, long period, TimeUnit unit) { + public static Observable interval(long initialDelay, long period, TimeUnit unit) { return interval(initialDelay, period, unit, Schedulers.computation()); } @@ -1402,7 +1402,7 @@ public final static Observable interval(long initialDelay, long period, Ti * @see ReactiveX operators documentation: Interval * @since 1.0.12 */ - public final static Observable interval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { + public static Observable interval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { return create(new OnSubscribeTimerPeriodically(initialDelay, period, unit, scheduler)); } @@ -1430,7 +1430,7 @@ public final static Observable interval(long initialDelay, long period, Ti * @return an Observable that emits {@code value} as a single item and then completes * @see ReactiveX operators documentation: Just */ - public final static Observable just(final T value) { + public static Observable just(final T value) { return ScalarSynchronousObservable.create(value); } @@ -1454,7 +1454,7 @@ public final static Observable just(final T value) { */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2) { + public static Observable just(T t1, T t2) { return from((T[])new Object[] { t1, t2 }); } @@ -1480,7 +1480,7 @@ public final static Observable just(T t1, T t2) { */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3) { + public static Observable just(T t1, T t2, T t3) { return from((T[])new Object[] { t1, t2, t3 }); } @@ -1508,7 +1508,7 @@ public final static Observable just(T t1, T t2, T t3) { */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3, T t4) { + public static Observable just(T t1, T t2, T t3, T t4) { return from((T[])new Object[] { t1, t2, t3, t4 }); } @@ -1538,7 +1538,7 @@ public final static Observable just(T t1, T t2, T t3, T t4) { */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3, T t4, T t5) { + public static Observable just(T t1, T t2, T t3, T t4, T t5) { return from((T[])new Object[] { t1, t2, t3, t4, t5 }); } @@ -1570,7 +1570,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5) { */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6) { + public static Observable just(T t1, T t2, T t3, T t4, T t5, T t6) { return from((T[])new Object[] { t1, t2, t3, t4, t5, t6 }); } @@ -1604,7 +1604,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6) { */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7) { + public static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7) { return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7 }); } @@ -1640,7 +1640,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8) { + public static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8) { return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7, t8 }); } @@ -1678,7 +1678,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8, T t9) { + public static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8, T t9) { return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7, t8, t9 }); } @@ -1718,7 +1718,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T */ // suppress unchecked because we are using varargs inside the method @SuppressWarnings("unchecked") - public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8, T t9, T t10) { + public static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T t7, T t8, T t9, T t10) { return from((T[])new Object[] { t1, t2, t3, t4, t5, t6, t7, t8, t9, t10 }); } @@ -1740,7 +1740,7 @@ public final static Observable just(T t1, T t2, T t3, T t4, T t5, T t6, T * Observables in the Iterable * @see ReactiveX operators documentation: Merge */ - public final static Observable merge(Iterable> sequences) { + public static Observable merge(Iterable> sequences) { return merge(from(sequences)); } @@ -1767,7 +1767,7 @@ public final static Observable merge(IterableReactiveX operators documentation: Merge */ - public final static Observable merge(Iterable> sequences, int maxConcurrent) { + public static Observable merge(Iterable> sequences, int maxConcurrent) { return merge(from(sequences), maxConcurrent); } @@ -1791,7 +1791,7 @@ public final static Observable merge(IterableReactiveX operators documentation: Merge */ @SuppressWarnings({"unchecked", "rawtypes"}) - public final static Observable merge(Observable> source) { + public static Observable merge(Observable> source) { if (source.getClass() == ScalarSynchronousObservable.class) { return ((ScalarSynchronousObservable)source).scalarFlatMap((Func1)UtilityFunctions.identity()); } @@ -1824,7 +1824,7 @@ public final static Observable merge(Observable Observable merge(Observable> source, int maxConcurrent) { + public static Observable merge(Observable> source, int maxConcurrent) { if (source.getClass() == ScalarSynchronousObservable.class) { return ((ScalarSynchronousObservable)source).scalarFlatMap((Func1)UtilityFunctions.identity()); } @@ -1851,7 +1851,7 @@ public final static Observable merge(ObservableReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2) { + public static Observable merge(Observable t1, Observable t2) { return merge(new Observable[] { t1, t2 }); } @@ -1877,7 +1877,7 @@ public final static Observable merge(Observable t1, Observab * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2, Observable t3) { + public static Observable merge(Observable t1, Observable t2, Observable t3) { return merge(new Observable[] { t1, t2, t3 }); } @@ -1905,7 +1905,7 @@ public final static Observable merge(Observable t1, Observab * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4) { + public static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4) { return merge(new Observable[] { t1, t2, t3, t4 }); } @@ -1935,7 +1935,7 @@ public final static Observable merge(Observable t1, Observab * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { + public static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { return merge(new Observable[] { t1, t2, t3, t4, t5 }); } @@ -1967,7 +1967,7 @@ public final static Observable merge(Observable t1, Observab * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { + public static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { return merge(new Observable[] { t1, t2, t3, t4, t5, t6 }); } @@ -2001,7 +2001,7 @@ public final static Observable merge(Observable t1, Observab * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { + public static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { return merge(new Observable[] { t1, t2, t3, t4, t5, t6, t7 }); } @@ -2037,7 +2037,7 @@ public final static Observable merge(Observable t1, Observab * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { + public static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { return merge(new Observable[] { t1, t2, t3, t4, t5, t6, t7, t8 }); } @@ -2075,7 +2075,7 @@ public final static Observable merge(Observable t1, Observab * @see ReactiveX operators documentation: Merge */ @SuppressWarnings("unchecked") - public final static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { + public static Observable merge(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { return merge(new Observable[] { t1, t2, t3, t4, t5, t6, t7, t8, t9 }); } @@ -2096,7 +2096,7 @@ public final static Observable merge(Observable t1, Observab * @return an Observable that emits all of the items emitted by the Observables in the Array * @see ReactiveX operators documentation: Merge */ - public final static Observable merge(Observable[] sequences) { + public static Observable merge(Observable[] sequences) { return merge(from(sequences)); } @@ -2121,7 +2121,7 @@ public final static Observable merge(Observable[] sequences) * @see ReactiveX operators documentation: Merge * @since 1.1.0 */ - public final static Observable merge(Observable[] sequences, int maxConcurrent) { + public static Observable merge(Observable[] sequences, int maxConcurrent) { return merge(from(sequences), maxConcurrent); } @@ -2149,7 +2149,7 @@ public final static Observable merge(Observable[] sequences, * {@code source} Observable * @see ReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable> source) { + public static Observable mergeDelayError(Observable> source) { return source.lift(OperatorMerge.instance(true)); } @@ -2182,7 +2182,7 @@ public final static Observable mergeDelayError(Observable Observable mergeDelayError(Observable> source, int maxConcurrent) { + public static Observable mergeDelayError(Observable> source, int maxConcurrent) { return source.lift(OperatorMerge.instance(true, maxConcurrent)); } @@ -2211,7 +2211,7 @@ public final static Observable mergeDelayError(ObservableReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable t1, Observable t2) { + public static Observable mergeDelayError(Observable t1, Observable t2) { return mergeDelayError(just(t1, t2)); } @@ -2243,7 +2243,7 @@ public final static Observable mergeDelayError(Observable t1 * @return an Observable that emits all of the items that are emitted by the source Observables * @see ReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3) { + public static Observable mergeDelayError(Observable t1, Observable t2, Observable t3) { return mergeDelayError(just(t1, t2, t3)); } @@ -2277,7 +2277,7 @@ public final static Observable mergeDelayError(Observable t1 * @return an Observable that emits all of the items that are emitted by the source Observables * @see ReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4) { + public static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4) { return mergeDelayError(just(t1, t2, t3, t4)); } @@ -2313,7 +2313,7 @@ public final static Observable mergeDelayError(Observable t1 * @return an Observable that emits all of the items that are emitted by the source Observables * @see ReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { + public static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5) { return mergeDelayError(just(t1, t2, t3, t4, t5)); } @@ -2351,7 +2351,7 @@ public final static Observable mergeDelayError(Observable t1 * @return an Observable that emits all of the items that are emitted by the source Observables * @see ReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { + public static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6) { return mergeDelayError(just(t1, t2, t3, t4, t5, t6)); } @@ -2392,7 +2392,7 @@ public final static Observable mergeDelayError(Observable t1 * @return an Observable that emits all of the items that are emitted by the source Observables * @see ReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { + public static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7) { return mergeDelayError(just(t1, t2, t3, t4, t5, t6, t7)); } @@ -2435,7 +2435,7 @@ public final static Observable mergeDelayError(Observable t1 * @see ReactiveX operators documentation: Merge */ // suppress because the types are checked by the method signature before using a vararg - public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { + public static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8) { return mergeDelayError(just(t1, t2, t3, t4, t5, t6, t7, t8)); } @@ -2479,7 +2479,7 @@ public final static Observable mergeDelayError(Observable t1 * @return an Observable that emits all of the items that are emitted by the source Observables * @see ReactiveX operators documentation: Merge */ - public final static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { + public static Observable mergeDelayError(Observable t1, Observable t2, Observable t3, Observable t4, Observable t5, Observable t6, Observable t7, Observable t8, Observable t9) { return mergeDelayError(just(t1, t2, t3, t4, t5, t6, t7, t8, t9)); } @@ -2516,7 +2516,7 @@ public final Observable> nest() { * @return an Observable that never emits any items or sends any notifications to an {@link Observer} * @see ReactiveX operators documentation: Never */ - public final static Observable never() { + public static Observable never() { return NeverObservable.instance(); } @@ -2539,7 +2539,7 @@ public final static Observable never() { * {@code Integer.MAX_VALUE} * @see ReactiveX operators documentation: Range */ - public final static Observable range(int start, int count) { + public static Observable range(int start, int count) { if (count < 0) { throw new IllegalArgumentException("Count can not be negative"); } @@ -2574,7 +2574,7 @@ public final static Observable range(int start, int count) { * @return an Observable that emits a range of sequential Integers * @see ReactiveX operators documentation: Range */ - public final static Observable range(int start, int count, Scheduler scheduler) { + public static Observable range(int start, int count, Scheduler scheduler) { return range(start, count).subscribeOn(scheduler); } @@ -2597,7 +2597,7 @@ public final static Observable range(int start, int count, Scheduler sc * @return an Observable that emits a Boolean value that indicates whether the two sequences are the same * @see ReactiveX operators documentation: SequenceEqual */ - public final static Observable sequenceEqual(Observable first, Observable second) { + public static Observable sequenceEqual(Observable first, Observable second) { return sequenceEqual(first, second, new Func2() { @Override public final Boolean call(T first, T second) { @@ -2632,7 +2632,7 @@ public final Boolean call(T first, T second) { * are the same according to the specified function * @see ReactiveX operators documentation: SequenceEqual */ - public final static Observable sequenceEqual(Observable first, Observable second, Func2 equality) { + public static Observable sequenceEqual(Observable first, Observable second, Func2 equality) { return OperatorSequenceEqual.sequenceEqual(first, second, equality); } @@ -2658,7 +2658,7 @@ public final static Observable sequenceEqual(ObservableReactiveX operators documentation: Switch */ - public final static Observable switchOnNext(Observable> sequenceOfSequences) { + public static Observable switchOnNext(Observable> sequenceOfSequences) { return sequenceOfSequences.lift(OperatorSwitch.instance()); } @@ -2687,7 +2687,7 @@ public final static Observable switchOnNext(Observable timer(long initialDelay, long period, TimeUnit unit) { + public static Observable timer(long initialDelay, long period, TimeUnit unit) { return interval(initialDelay, period, unit, Schedulers.computation()); } @@ -2718,7 +2718,7 @@ public final static Observable timer(long initialDelay, long period, TimeU * @deprecated use {@link #interval(long, long, TimeUnit, Scheduler)} instead */ @Deprecated - public final static Observable timer(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { + public static Observable timer(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { return interval(initialDelay, period, unit, scheduler); } @@ -2741,7 +2741,7 @@ public final static Observable timer(long initialDelay, long period, TimeU * @return an Observable that emits one item after a specified delay, and then completes * @see ReactiveX operators documentation: Timer */ - public final static Observable timer(long delay, TimeUnit unit) { + public static Observable timer(long delay, TimeUnit unit) { return timer(delay, unit, Schedulers.computation()); } @@ -2768,7 +2768,7 @@ public final static Observable timer(long delay, TimeUnit unit) { * completes * @see ReactiveX operators documentation: Timer */ - public final static Observable timer(long delay, TimeUnit unit, Scheduler scheduler) { + public static Observable timer(long delay, TimeUnit unit, Scheduler scheduler) { return create(new OnSubscribeTimerOnce(delay, unit, scheduler)); } @@ -2790,7 +2790,7 @@ public final static Observable timer(long delay, TimeUnit unit, Scheduler * @return the Observable whose lifetime controls the lifetime of the dependent resource object * @see ReactiveX operators documentation: Using */ - public final static Observable using( + public static Observable using( final Func0 resourceFactory, final Func1> observableFactory, final Action1 disposeAction) { @@ -2826,7 +2826,7 @@ public final static Observable using( * @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number) */ @Experimental - public final static Observable using( + public static Observable using( final Func0 resourceFactory, final Func1> observableFactory, final Action1 disposeAction, boolean disposeEagerly) { @@ -2859,7 +2859,7 @@ public final static Observable using( * @return an Observable that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Observable zip(Iterable> ws, FuncN zipFunction) { + public static Observable zip(Iterable> ws, FuncN zipFunction) { List> os = new ArrayList>(); for (Observable o : ws) { os.add(o); @@ -2893,7 +2893,7 @@ public final static Observable zip(Iterable> ws, * @return an Observable that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Observable zip(Observable> ws, final FuncN zipFunction) { + public static Observable zip(Observable> ws, final FuncN zipFunction) { return ws.toList().map(new Func1>, Observable[]>() { @Override @@ -2933,7 +2933,7 @@ public Observable[] call(List> o) { * @return an Observable that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, final Func2 zipFunction) { + public static Observable zip(Observable o1, Observable o2, final Func2 zipFunction) { return just(new Observable[] { o1, o2 }).lift(new OperatorZip(zipFunction)); } @@ -2969,7 +2969,7 @@ public final static Observable zip(Observable o1, O * @return an Observable that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, Observable o3, Func3 zipFunction) { + public static Observable zip(Observable o1, Observable o2, Observable o3, Func3 zipFunction) { return just(new Observable[] { o1, o2, o3 }).lift(new OperatorZip(zipFunction)); } @@ -3007,7 +3007,7 @@ public final static Observable zip(Observable o * @return an Observable that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Func4 zipFunction) { + public static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Func4 zipFunction) { return just(new Observable[] { o1, o2, o3, o4 }).lift(new OperatorZip(zipFunction)); } @@ -3047,7 +3047,7 @@ public final static Observable zip(ObservableReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Func5 zipFunction) { + public static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Func5 zipFunction) { return just(new Observable[] { o1, o2, o3, o4, o5 }).lift(new OperatorZip(zipFunction)); } @@ -3088,7 +3088,7 @@ public final static Observable zip(ObservableReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, + public static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Func6 zipFunction) { return just(new Observable[] { o1, o2, o3, o4, o5, o6 }).lift(new OperatorZip(zipFunction)); } @@ -3132,7 +3132,7 @@ public final static Observable zip(ObservableReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, + public static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Func7 zipFunction) { return just(new Observable[] { o1, o2, o3, o4, o5, o6, o7 }).lift(new OperatorZip(zipFunction)); } @@ -3178,7 +3178,7 @@ public final static Observable zip(Observable * @return an Observable that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, + public static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Func8 zipFunction) { return just(new Observable[] { o1, o2, o3, o4, o5, o6, o7, o8 }).lift(new OperatorZip(zipFunction)); } @@ -3226,7 +3226,7 @@ public final static Observable zip(Observ * @return an Observable that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public final static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, + public static Observable zip(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9, Func9 zipFunction) { return just(new Observable[] { o1, o2, o3, o4, o5, o6, o7, o8, o9 }).lift(new OperatorZip(zipFunction)); } @@ -5870,7 +5870,7 @@ public final Observable map(Func1 func) { return lift(new OperatorMap(func)); } - private final Observable mapNotification(Func1 onNext, Func1 onError, Func0 onCompleted) { + private Observable mapNotification(Func1 onNext, Func1 onError, Func0 onCompleted) { return lift(new OperatorMapNotification(onNext, onError, onCompleted)); } diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index fb1fed35d7..5ffbee393b 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -139,7 +139,7 @@ private Single(final Observable.OnSubscribe f) { * @return a Single that, when a {@link Subscriber} subscribes to it, will execute the specified function * @see ReactiveX operators documentation: Create */ - public final static Single create(OnSubscribe f) { + public static Single create(OnSubscribe f) { return new Single(f); // TODO need hook } @@ -172,7 +172,7 @@ 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 final Single lift(final Operator lift) { + 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 return new Single(new Observable.OnSubscribe() { @@ -257,7 +257,7 @@ private static Observable asObservable(Single t) { * @return a Single that emits an Observable that emits the same item as the source Single * @see ReactiveX operators documentation: To */ - private final Single> nest() { + private Single> nest() { return Single.just(asObservable(this)); } @@ -282,7 +282,7 @@ private final Single> nest() { * @return an Observable that emits items emitted by the two source Singles, one after the other. * @see ReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2) { + public static Observable concat(Single t1, Single t2) { return Observable.concat(asObservable(t1), asObservable(t2)); } @@ -304,7 +304,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2, Single t3) { + public static Observable concat(Single t1, Single t2, Single t3) { return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3)); } @@ -328,7 +328,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2, Single t3, Single t4) { + public static Observable concat(Single t1, Single t2, Single t3, Single t4) { return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4)); } @@ -354,7 +354,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5) { + public static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5) { return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5)); } @@ -382,7 +382,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { + public static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6)); } @@ -412,7 +412,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { + public static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7)); } @@ -444,7 +444,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { + public static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8)); } @@ -478,7 +478,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Concat */ - public final static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { + public static Observable concat(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { return Observable.concat(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8), asObservable(t9)); } @@ -500,7 +500,7 @@ public final static Observable concat(Single t1, SingleReactiveX operators documentation: Throw */ - public final static Single error(final Throwable exception) { + public static Single error(final Throwable exception) { return Single.create(new OnSubscribe() { @Override @@ -534,7 +534,7 @@ public void call(SingleSubscriber te) { * @return a {@code Single} that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ - public final static Single from(Future future) { + public static Single from(Future future) { return new Single(OnSubscribeToObservableFuture.toObservableFuture(future)); } @@ -565,7 +565,7 @@ public final static Single from(Future future) { * @return a {@code Single} that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ - public final static Single from(Future future, long timeout, TimeUnit unit) { + public static Single from(Future future, long timeout, TimeUnit unit) { return new Single(OnSubscribeToObservableFuture.toObservableFuture(future, timeout, unit)); } @@ -593,7 +593,7 @@ public final static Single from(Future future, long timeout, * @return a {@code Single} that emits the item from the source {@link Future} * @see ReactiveX operators documentation: From */ - public final static Single from(Future future, Scheduler scheduler) { + public static Single from(Future future, Scheduler scheduler) { return new Single(OnSubscribeToObservableFuture.toObservableFuture(future)).subscribeOn(scheduler); } @@ -653,7 +653,7 @@ public void call(SingleSubscriber singleSubscriber) { * @return a {@code Single} that emits {@code value} * @see ReactiveX operators documentation: Just */ - public final static Single just(final T value) { + public static Single just(final T value) { // TODO add similar optimization as ScalarSynchronousObservable return Single.create(new OnSubscribe() { @@ -682,7 +682,7 @@ public void call(SingleSubscriber te) { * by {@code source} * @see ReactiveX operators documentation: Merge */ - public final static Single merge(final Single> source) { + public static Single merge(final Single> source) { return Single.create(new OnSubscribe() { @Override @@ -723,7 +723,7 @@ public void onError(Throwable error) { * @return an Observable that emits all of the items emitted by the source Singles * @see ReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2) { + public static Observable merge(Single t1, Single t2) { return Observable.merge(asObservable(t1), asObservable(t2)); } @@ -748,7 +748,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2, Single t3) { + public static Observable merge(Single t1, Single t2, Single t3) { return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3)); } @@ -775,7 +775,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2, Single t3, Single t4) { + public static Observable merge(Single t1, Single t2, Single t3, Single t4) { return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4)); } @@ -804,7 +804,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5) { + public static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5) { return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5)); } @@ -835,7 +835,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { + public static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6) { return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6)); } @@ -868,7 +868,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { + public static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7) { return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7)); } @@ -903,7 +903,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { + public static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8) { return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8)); } @@ -940,7 +940,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Merge */ - public final static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { + public static Observable merge(Single t1, Single t2, Single t3, Single t4, Single t5, Single t6, Single t7, Single t8, Single t9) { return Observable.merge(asObservable(t1), asObservable(t2), asObservable(t3), asObservable(t4), asObservable(t5), asObservable(t6), asObservable(t7), asObservable(t8), asObservable(t9)); } @@ -964,7 +964,7 @@ public final static Observable merge(Single t1, SingleReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, final Func2 zipFunction) { + public static Single zip(Single s1, Single s2, final Func2 zipFunction) { return SingleOperatorZip.zip(new Single[] {s1, s2}, new FuncN() { @Override public R call(Object... args) { @@ -995,7 +995,7 @@ public R call(Object... args) { * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, Single s3, final Func3 zipFunction) { + public static Single zip(Single s1, Single s2, Single s3, final Func3 zipFunction) { return SingleOperatorZip.zip(new Single[] {s1, s2, s3}, new FuncN() { @Override public R call(Object... args) { @@ -1028,7 +1028,7 @@ public R call(Object... args) { * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, Single s3, Single s4, final Func4 zipFunction) { + 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() { @Override public R call(Object... args) { @@ -1063,7 +1063,7 @@ public R call(Object... args) { * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, final Func5 zipFunction) { + 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() { @Override public R call(Object... args) { @@ -1100,7 +1100,7 @@ public R call(Object... args) { * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, + 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() { @Override @@ -1140,7 +1140,7 @@ public R call(Object... args) { * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, Single s7, + 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() { @Override @@ -1182,7 +1182,7 @@ public R call(Object... args) { * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, Single s7, Single s8, + 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() { @Override @@ -1226,7 +1226,7 @@ public R call(Object... args) { * @return a Single that emits the zipped results * @see ReactiveX operators documentation: Zip */ - public static final Single zip(Single s1, Single s2, Single s3, Single s4, Single s5, Single s6, Single s7, Single s8, + 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() { @Override diff --git a/src/main/java/rx/exceptions/CompositeException.java b/src/main/java/rx/exceptions/CompositeException.java index 79a49a7e74..17344ac426 100644 --- a/src/main/java/rx/exceptions/CompositeException.java +++ b/src/main/java/rx/exceptions/CompositeException.java @@ -247,7 +247,7 @@ public String getMessage() { } } - private final List getListOfCauses(Throwable ex) { + private List getListOfCauses(Throwable ex) { List list = new ArrayList(); Throwable root = ex.getCause(); if (root == null) { diff --git a/src/main/java/rx/internal/operators/OnSubscribeAmb.java b/src/main/java/rx/internal/operators/OnSubscribeAmb.java index 2ba94b0f97..c5d611375d 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeAmb.java +++ b/src/main/java/rx/internal/operators/OnSubscribeAmb.java @@ -278,7 +278,7 @@ private static final class AmbSubscriber extends Subscriber { request(requested); } - private final void requestMore(long n) { + private void requestMore(long n) { request(n); } diff --git a/src/main/java/rx/internal/util/IndexedRingBuffer.java b/src/main/java/rx/internal/util/IndexedRingBuffer.java index 4bf941dee1..73f921b30d 100644 --- a/src/main/java/rx/internal/util/IndexedRingBuffer.java +++ b/src/main/java/rx/internal/util/IndexedRingBuffer.java @@ -58,7 +58,7 @@ protected IndexedRingBuffer createObject() { }; @SuppressWarnings("unchecked") - public final static IndexedRingBuffer getInstance() { + public static IndexedRingBuffer getInstance() { return (IndexedRingBuffer) POOL.borrowObject(); } diff --git a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java index 797a4e4406..f4c8c3cd2e 100644 --- a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java +++ b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java @@ -78,7 +78,7 @@ static Producer createProducer(Subscriber s, T v) { * @param t the value to emit when requested * @return the new Observable */ - public static final ScalarSynchronousObservable create(T t) { + public static ScalarSynchronousObservable create(T t) { return new ScalarSynchronousObservable(t); } diff --git a/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java index 5a00430b96..33472a40da 100644 --- a/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java +++ b/src/main/java/rx/internal/util/atomic/SpscLinkedArrayQueue.java @@ -242,17 +242,17 @@ private void soConsumerIndex(long v) { CONSUMER_INDEX.lazySet(this, v); } - private static final int calcWrappedOffset(long index, int mask) { + private static int calcWrappedOffset(long index, int mask) { return calcDirectOffset((int)index & mask); } - private static final int calcDirectOffset(int index) { + private static int calcDirectOffset(int index) { return index; } - private static final void soElement(AtomicReferenceArray buffer, int offset, Object e) { + private static void soElement(AtomicReferenceArray buffer, int offset, Object e) { buffer.lazySet(offset, e); } - private static final Object lvElement(AtomicReferenceArray buffer, int offset) { + private static Object lvElement(AtomicReferenceArray buffer, int offset) { return buffer.get(offset); } diff --git a/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java index af62a9ce60..54bdfeba5a 100644 --- a/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java +++ b/src/main/java/rx/internal/util/atomic/SpscUnboundedAtomicArrayQueue.java @@ -243,17 +243,17 @@ private void soConsumerIndex(long v) { CONSUMER_INDEX.lazySet(this, v); } - private static final int calcWrappedOffset(long index, int mask) { + private static int calcWrappedOffset(long index, int mask) { return calcDirectOffset((int)index & mask); } - private static final int calcDirectOffset(int index) { + private static int calcDirectOffset(int index) { return index; } - private static final void soElement(AtomicReferenceArray buffer, int offset, Object e) { + private static void soElement(AtomicReferenceArray buffer, int offset, Object e) { buffer.lazySet(offset, e); } - private static final Object lvElement(AtomicReferenceArray buffer, int offset) { + private static Object lvElement(AtomicReferenceArray buffer, int offset) { return buffer.get(offset); } diff --git a/src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java index c579864549..680f62860a 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscUnboundedArrayQueue.java @@ -264,17 +264,17 @@ private void soConsumerIndex(long v) { UNSAFE.putOrderedLong(this, C_INDEX_OFFSET, v); } - private static final long calcWrappedOffset(long index, long mask) { + private static long calcWrappedOffset(long index, long mask) { return calcDirectOffset(index & mask); } - private static final long calcDirectOffset(long index) { + private static long calcDirectOffset(long index) { return REF_ARRAY_BASE + (index << REF_ELEMENT_SHIFT); } - private static final void soElement(Object[] buffer, long offset, Object e) { + private static void soElement(Object[] buffer, long offset, Object e) { UNSAFE.putOrderedObject(buffer, offset, e); } - private static final Object lvElement(E[] buffer, long offset) { + private static Object lvElement(E[] buffer, long offset) { return UNSAFE.getObjectVolatile(buffer, offset); } diff --git a/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java b/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java index 88d0ebf4dd..a13989f4f1 100644 --- a/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java +++ b/src/main/java/rx/internal/util/unsafe/UnsafeAccess.java @@ -47,7 +47,7 @@ private UnsafeAccess() { UNSAFE = u; } - public static final boolean isUnsafeAvailable() { + public static boolean isUnsafeAvailable() { return UNSAFE != null; } diff --git a/src/main/java/rx/observables/GroupedObservable.java b/src/main/java/rx/observables/GroupedObservable.java index 8daea16643..ad9ceb9370 100644 --- a/src/main/java/rx/observables/GroupedObservable.java +++ b/src/main/java/rx/observables/GroupedObservable.java @@ -85,7 +85,7 @@ public void call(Subscriber s) { * @return a GroupedObservable that, when a {@link Subscriber} subscribes to it, will execute the specified * function */ - public final static GroupedObservable create(K key, OnSubscribe f) { + public static GroupedObservable create(K key, OnSubscribe f) { return new GroupedObservable(key, f); } diff --git a/src/main/java/rx/observers/Observers.java b/src/main/java/rx/observers/Observers.java index 090585b256..8425d62b6c 100644 --- a/src/main/java/rx/observers/Observers.java +++ b/src/main/java/rx/observers/Observers.java @@ -71,7 +71,7 @@ public static Observer empty() { * @return an {@code Observer} that calls {@code onNext} for each emitted item from the {@code Observable} * the {@code Observer} subscribes to */ - public static final Observer create(final Action1 onNext) { + public static Observer create(final Action1 onNext) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } @@ -111,7 +111,7 @@ public final void onNext(T args) { * the {@code Observer} subscribes to, and calls {@code onError} if the {@code Observable} notifies * of an error */ - public static final Observer create(final Action1 onNext, final Action1 onError) { + public static Observer create(final Action1 onNext, final Action1 onError) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } @@ -157,7 +157,7 @@ public final void onNext(T args) { * of an error, and calls {@code onComplete} if the {@code Observable} notifies that the observable * sequence is complete */ - public static final Observer create(final Action1 onNext, final Action1 onError, final Action0 onComplete) { + public static Observer create(final Action1 onNext, final Action1 onError, final Action0 onComplete) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } diff --git a/src/main/java/rx/observers/Subscribers.java b/src/main/java/rx/observers/Subscribers.java index c1d2e4d014..f29f2c8a93 100644 --- a/src/main/java/rx/observers/Subscribers.java +++ b/src/main/java/rx/observers/Subscribers.java @@ -79,7 +79,7 @@ public void onNext(T t) { * @return a {@code Subscriber} that calls {@code onNext} for each emitted item from the {@code Observable} * the {@code Subscriber} subscribes to */ - public static final Subscriber create(final Action1 onNext) { + public static Subscriber create(final Action1 onNext) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } @@ -119,7 +119,7 @@ public final void onNext(T args) { * the {@code Subscriber} subscribes to, and calls {@code onError} if the {@code Observable} * notifies of an error */ - public static final Subscriber create(final Action1 onNext, final Action1 onError) { + public static Subscriber create(final Action1 onNext, final Action1 onError) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } @@ -165,7 +165,7 @@ public final void onNext(T args) { * of an error, and calls {@code onComplete} if the {@code Observable} notifies that the observable * sequence is complete */ - public static final Subscriber create(final Action1 onNext, final Action1 onError, final Action0 onComplete) { + public static Subscriber create(final Action1 onNext, final Action1 onError, final Action0 onComplete) { if (onNext == null) { throw new IllegalArgumentException("onNext can not be null"); } diff --git a/src/main/java/rx/subjects/ReplaySubject.java b/src/main/java/rx/subjects/ReplaySubject.java index ca166b6177..6b5b43c799 100644 --- a/src/main/java/rx/subjects/ReplaySubject.java +++ b/src/main/java/rx/subjects/ReplaySubject.java @@ -299,7 +299,7 @@ public static ReplaySubject createWithTimeAndSize(long time, TimeUnit uni * the shared state * @return the created subject */ - static final ReplaySubject createWithState(final BoundedState state, + static ReplaySubject createWithState(final BoundedState state, Action1> onStart) { SubjectSubscriptionManager ssm = new SubjectSubscriptionManager(); ssm.onStart = onStart; From 6e1b55fbd86fff2e9667e7667eacb27fd66167a4 Mon Sep 17 00:00:00 2001 From: msavitskiy Date: Sun, 17 Jan 2016 03:11:49 +0200 Subject: [PATCH 496/857] fix for issue 3599 Move line to try block. For avoid assigned twice. --- .../rx/exceptions/CompositeException.java | 3 +- .../rx/exceptions/CompositeExceptionTest.java | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/exceptions/CompositeException.java b/src/main/java/rx/exceptions/CompositeException.java index 79a49a7e74..d251cf2e95 100644 --- a/src/main/java/rx/exceptions/CompositeException.java +++ b/src/main/java/rx/exceptions/CompositeException.java @@ -118,12 +118,13 @@ public synchronized Throwable getCause() { // we now have 'e' as the last in the chain try { chain.initCause(e); + chain = chain.getCause(); } catch (Throwable t) { // ignore // the javadocs say that some Throwables (depending on how they're made) will never // let me call initCause without blowing up even if it returns null + chain = e; } - chain = chain.getCause(); } cause = _cause; } diff --git a/src/test/java/rx/exceptions/CompositeExceptionTest.java b/src/test/java/rx/exceptions/CompositeExceptionTest.java index 5fadadd42c..fc28e5b21b 100644 --- a/src/test/java/rx/exceptions/CompositeExceptionTest.java +++ b/src/test/java/rx/exceptions/CompositeExceptionTest.java @@ -178,4 +178,46 @@ public void testNullElement() { composite.getCause(); composite.printStackTrace(); } + + @Test(timeout = 1000) + public void testCompositeExceptionWithUnsupportedInitCause() { + Throwable t = new Throwable() { + @Override + public synchronized Throwable initCause(Throwable cause) { + throw new UnsupportedOperationException(); + } + }; + CompositeException cex = new CompositeException(Arrays.asList(t, ex1)); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(2, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + @Test(timeout = 1000) + public void testCompositeExceptionWithNullInitCause() { + Throwable t = new Throwable("ThrowableWithNullInitCause") { + @Override + public synchronized Throwable initCause(Throwable cause) { + return null; + } + }; + CompositeException cex = new CompositeException(Arrays.asList(t, ex1)); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(2, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } } \ No newline at end of file From 995d3f166a0a23d91a5102e26583a621b684d677 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 28 Jan 2016 16:36:04 +0100 Subject: [PATCH 497/857] 1.x: fix sample(Observable) not requesting Long.MAX_VALUE --- .../OperatorSampleWithObservable.java | 4 +- .../operators/OperatorSampleTest.java | 45 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java b/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java index a89b419025..3b3e295dd3 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java @@ -69,7 +69,7 @@ public void onCompleted() { }; - Subscriber result = new Subscriber(child) { + Subscriber result = new Subscriber() { @Override public void onNext(T t) { value.set(t); @@ -88,6 +88,8 @@ public void onCompleted() { } }; + child.add(result); + sampler.unsafeSubscribe(samplerSub); return result; diff --git a/src/test/java/rx/internal/operators/OperatorSampleTest.java b/src/test/java/rx/internal/operators/OperatorSampleTest.java index 2ef1ae8fb3..1db795cbfb 100644 --- a/src/test/java/rx/internal/operators/OperatorSampleTest.java +++ b/src/test/java/rx/internal/operators/OperatorSampleTest.java @@ -16,21 +16,16 @@ package rx.internal.operators; 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 static org.mockito.Mockito.*; import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; +import org.junit.*; import org.mockito.InOrder; import rx.*; import rx.Observable.OnSubscribe; -import rx.functions.Action0; +import rx.functions.*; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; @@ -283,4 +278,38 @@ public void call(Subscriber subscriber) { o.throttleLast(1, TimeUnit.MILLISECONDS).subscribe().unsubscribe(); verify(s).unsubscribe(); } + + @Test + public void testSampleOtherUnboundedIn() { + + final long[] requested = { -1 }; + + PublishSubject.create() + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requested[0] = t; + } + }) + .sample(PublishSubject.create()).subscribe(); + + Assert.assertEquals(Long.MAX_VALUE, requested[0]); + } + + @Test + public void testSampleTimedUnboundedIn() { + + final long[] requested = { -1 }; + + PublishSubject.create() + .doOnRequest(new Action1() { + @Override + public void call(Long t) { + requested[0] = t; + } + }) + .sample(1, TimeUnit.SECONDS).subscribe().unsubscribe(); + + Assert.assertEquals(Long.MAX_VALUE, requested[0]); + } } From 67ef32c2c3b28cf3dd40e999bc836deca52fda20 Mon Sep 17 00:00:00 2001 From: Artem Zinnatullin Date: Fri, 29 Jan 2016 04:36:34 +0300 Subject: [PATCH 498/857] Add Single.onErrorResumeNext(Single) --- src/main/java/rx/Single.java | 31 +++++++++++++ ...gleOperatorOnErrorResumeNextViaSingle.java | 45 +++++++++++++++++++ src/test/java/rx/SingleTest.java | 42 +++++++++++++++-- 3 files changed, 115 insertions(+), 3 deletions(-) create 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 fb1fed35d7..96ac18a25a 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1411,6 +1411,37 @@ public final Single onErrorReturn(Func1 resumeFunctio return lift(new OperatorOnErrorReturn(resumeFunction)); } + /** + * 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 another Single ({@code resumeSingleInCaseOfError}) 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 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 + */ + public final Single onErrorResumeNext(Single resumeSingleInCaseOfError) { + return new Single(new SingleOperatorOnErrorResumeNextViaSingle(this, resumeSingleInCaseOfError)); + } + /** * Subscribes to a Single but ignore its emission or notification. *
    diff --git a/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNextViaSingle.java b/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNextViaSingle.java new file mode 100644 index 0000000000..ca47f9c3e9 --- /dev/null +++ b/src/main/java/rx/internal/operators/SingleOperatorOnErrorResumeNextViaSingle.java @@ -0,0 +1,45 @@ +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 b29fcb01af..15de891636 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -1,11 +1,11 @@ /** * Copyright 2015 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. @@ -1182,6 +1182,42 @@ public void doAfterTerminateActionShouldNotBeInvokedUntilSubscriberSubscribes() verifyZeroInteractions(action); } + @Test + public void onErrorResumeNextViaSingleShouldNotInterruptSuccessfulSingle() { + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .just("success") + .onErrorResumeNext(Single.just("fail")) + .subscribe(testSubscriber); + + testSubscriber.assertValue("success"); + } + + @Test + public void onErrorResumeNextViaSingleShouldResumeWithPassedSingleInCaseOfError() { + TestSubscriber testSubscriber = new TestSubscriber(); + + Single + .error(new RuntimeException("test exception")) + .onErrorResumeNext(Single.just("fallback")) + .subscribe(testSubscriber); + + testSubscriber.assertValue("fallback"); + } + + @Test + public void onErrorResumeNextViaSingleShouldPreventNullSingle() { + try { + Single + .just("value") + .onErrorResumeNext(null); + fail(); + } catch (NullPointerException expected) { + assertEquals("resumeSingleInCaseOfError must not be null", expected.getMessage()); + } + } + @Test(expected = NullPointerException.class) public void iterableToArrayShouldThrowNullPointerExceptionIfIterableNull() { Single.iterableToArray(null); From dc2e516f9dc31b076be7926ec40f77ff83c7f8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 1 Feb 2016 22:33:39 +0100 Subject: [PATCH 499/857] 1.x: CombineLatest now supports any number of sources --- src/main/java/rx/Observable.java | 25 + .../operators/OnSubscribeCombineLatest.java | 606 ++++++++++-------- .../OnSubscribeCombineLatestTest.java | 128 +++- 3 files changed, 473 insertions(+), 286 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7754f87c43..6e28c5d4fe 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -810,6 +810,31 @@ public static Observable combineLatest(List(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. + *
    + *
    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 combineLatest(Iterable> sources, FuncN combineFunction) { + return create(new OnSubscribeCombineLatest(sources, combineFunction)); + } + /** * 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 5df99b2585..152a0831b0 100644 --- a/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java +++ b/src/main/java/rx/internal/operators/OnSubscribeCombineLatest.java @@ -1,317 +1,409 @@ /** - * 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 - * + * Copyright 2015 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. + * + * 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.BitSet; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; +import java.util.*; +import java.util.concurrent.atomic.*; +import rx.*; import rx.Observable; import rx.Observable.OnSubscribe; -import rx.exceptions.*; -import rx.Producer; -import rx.Subscriber; +import rx.exceptions.CompositeException; import rx.functions.FuncN; import rx.internal.util.RxRingBuffer; +import rx.internal.util.atomic.SpscLinkedArrayQueue; +import rx.plugins.RxJavaPlugins; -/** - * Returns an Observable that combines the emissions of multiple source observables. Once each - * source Observable has emitted at least one item, combineLatest emits an item whenever any of - * the source Observables emits an item, by combining the latest emissions from each source - * Observable with a specified function. - *

    - * - * - * @param - * the common basetype of the source values - * @param - * the result type of the combinator function - */ public final class OnSubscribeCombineLatest implements OnSubscribe { - final List> sources; - final FuncN combinator; - - public OnSubscribeCombineLatest(List> sources, FuncN combinator) { + final Observable[] sources; + final Iterable> sourcesIterable; + final FuncN combiner; + final int bufferSize; + final boolean delayError; + + public OnSubscribeCombineLatest(Iterable> sourcesIterable, + FuncN combiner) { + this(null, sourcesIterable, combiner, RxRingBuffer.SIZE, false); + } + + public OnSubscribeCombineLatest(Observable[] sources, + Iterable> sourcesIterable, + FuncN combiner, int bufferSize, + boolean delayError) { this.sources = sources; - this.combinator = combinator; - if (sources.size() > RxRingBuffer.SIZE) { - // For design simplicity this is limited to RxRingBuffer.SIZE. If more are really needed we'll need to - // adjust the design of how RxRingBuffer is used in the implementation below. - throw new IllegalArgumentException("More than RxRingBuffer.SIZE sources to combineLatest is not supported."); - } + this.sourcesIterable = sourcesIterable; + this.combiner = combiner; + this.bufferSize = bufferSize; + this.delayError = delayError; } + @Override - public void call(final Subscriber child) { - if (sources.isEmpty()) { - child.onCompleted(); - return; - } - if (sources.size() == 1) { - child.setProducer(new SingleSourceProducer(child, sources.get(0), combinator)); + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void call(Subscriber s) { + Observable[] sources = this.sources; + int count = 0; + if (sources == null) { + if (sourcesIterable instanceof List) { + // unchecked & raw: javac type inference problem otherwise + List list = (List)sourcesIterable; + sources = (Observable[])list.toArray(new Observable[list.size()]); + count = sources.length; + } else { + sources = new Observable[8]; + for (Observable p : sourcesIterable) { + if (count == sources.length) { + Observable[] b = new Observable[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = p; + } + } } else { - child.setProducer(new MultiSourceProducer(child, sources, combinator)); + count = sources.length; } - + + if (count == 0) { + s.onCompleted(); + return; + } + + LatestCoordinator lc = new LatestCoordinator(s, combiner, count, bufferSize, delayError); + lc.subscribe(sources); } - - /* - * benjchristensen => This implementation uses a buffer enqueue/drain pattern. It could be optimized to have a fast-path to - * skip the buffer and emit directly when no conflict, but that is quite complicated and I don't have the time to attempt it right now. - */ - final static class MultiSourceProducer implements Producer { - private final AtomicBoolean started = new AtomicBoolean(); - private final AtomicLong requested = new AtomicLong(); - private final List> sources; - private final Subscriber child; - private final FuncN combinator; - private final MultiSourceRequestableSubscriber[] subscribers; - - /* following are guarded by WIP */ - private final RxRingBuffer buffer = RxRingBuffer.getSpmcInstance(); - private final Object[] collectedValues; - private final BitSet haveValues; - private volatile int haveValuesCount; // does this need to be volatile or is WIP sufficient? - private final BitSet completion; - private volatile int completionCount; // does this need to be volatile or is WIP sufficient? - - private final AtomicLong counter = new AtomicLong(); - + + static final class LatestCoordinator extends AtomicInteger implements Producer, Subscription { + /** */ + private static final long serialVersionUID = 8567835998786448817L; + final Subscriber actual; + final FuncN combiner; + final int count; + final CombinerSubscriber[] subscribers; + final int bufferSize; + final Object[] latest; + final SpscLinkedArrayQueue queue; + final boolean delayError; + + volatile boolean cancelled; + + volatile boolean done; + + final AtomicLong requested; + + final AtomicReference error; + + int active; + int complete; + + /** Indicates the particular source hasn't emitted any value yet. */ + static final Object MISSING = new Object(); + @SuppressWarnings("unchecked") - public MultiSourceProducer(final Subscriber child, final List> sources, FuncN combinator) { - this.sources = sources; - this.child = child; - this.combinator = combinator; - - int n = sources.size(); - this.subscribers = new MultiSourceRequestableSubscriber[n]; - this.collectedValues = new Object[n]; - this.haveValues = new BitSet(n); - this.completion = new BitSet(n); + public LatestCoordinator(Subscriber actual, + FuncN combiner, + int count, int bufferSize, boolean delayError) { + this.actual = actual; + this.combiner = combiner; + this.count = count; + this.bufferSize = bufferSize; + this.delayError = delayError; + this.latest = new Object[count]; + Arrays.fill(latest, MISSING); + this.subscribers = new CombinerSubscriber[count]; + this.queue = new SpscLinkedArrayQueue(bufferSize); + this.requested = new AtomicLong(); + this.error = new AtomicReference(); } - + + public void subscribe(Observable[] sources) { + Subscriber[] as = subscribers; + int len = as.length; + for (int i = 0; i < len; i++) { + as[i] = new CombinerSubscriber(this, i); + } + lazySet(0); // release array contents + actual.add(this); + actual.setProducer(this); + for (int i = 0; i < len; i++) { + if (cancelled) { + return; + } + sources[i].subscribe(as[i]); + } + } + @Override public void request(long n) { - BackpressureUtils.getAndAddRequest(requested, n); - if (!started.get() && started.compareAndSet(false, true)) { - /* - * NOTE: this logic will ONLY work if we don't have more sources than the size of the buffer. - * - * We would likely need to make an RxRingBuffer that can be sized to [numSources * n] instead - * of the current global default size it has. - */ - int sizePerSubscriber = RxRingBuffer.SIZE / sources.size(); - int leftOver = RxRingBuffer.SIZE % sources.size(); - for (int i = 0; i < sources.size(); i++) { - Observable o = sources.get(i); - int toRequest = sizePerSubscriber; - if (i == sources.size() - 1) { - toRequest += leftOver; - } - MultiSourceRequestableSubscriber s = new MultiSourceRequestableSubscriber(i, toRequest, child, this); - subscribers[i] = s; - o.unsafeSubscribe(s); + if (n < 0) { + throw new IllegalArgumentException("n >= required but it was " + n); + } + if (n != 0) { + BackpressureUtils.getAndAddRequest(requested, n); + drain(); + } + } + + @Override + public void unsubscribe() { + if (!cancelled) { + cancelled = true; + + if (getAndIncrement() == 0) { + cancel(queue); } } - tick(); } - + + @Override + public boolean isUnsubscribed() { + return cancelled; + } + + void cancel(Queue q) { + q.clear(); + for (CombinerSubscriber s : subscribers) { + s.unsubscribe(); + } + } + /** - * This will only allow one thread at a time to do the work, but ensures via `counter` increment/decrement - * that there is always once who acts on each `tick`. Same concept as used in OperationObserveOn. + * Combine the given notification value from the indexth source with the existing known + * latest values. + * @param value the notification to combine, null indicates the source terminated normally + * @param index the index of the source subscriber */ - void tick() { - AtomicLong localCounter = this.counter; - if (localCounter.getAndIncrement() == 0) { - int emitted = 0; - do { - // we only emit if requested > 0 - if (requested.get() > 0) { - Object o = buffer.poll(); - if (o != null) { - if (buffer.isCompleted(o)) { - child.onCompleted(); - } else { - buffer.accept(o, child); - emitted++; - requested.decrementAndGet(); - } - } - } - } while (localCounter.decrementAndGet() > 0); - if (emitted > 0) { - for (MultiSourceRequestableSubscriber s : subscribers) { - s.requestUpTo(emitted); + void combine(Object value, int index) { + CombinerSubscriber combinerSubscriber = subscribers[index]; + + int activeCount; + int completedCount; + int sourceCount; + boolean empty; + boolean allSourcesFinished; + synchronized (this) { + sourceCount = latest.length; + Object o = latest[index]; + activeCount = active; + if (o == MISSING) { + active = ++activeCount; + } + completedCount = complete; + if (value == null) { + complete = ++completedCount; + } else { + latest[index] = combinerSubscriber.nl.getValue(value); + } + allSourcesFinished = activeCount == sourceCount; + // see if either all sources completed + empty = completedCount == sourceCount + || (value == null && o == MISSING); // or this source completed without any value + if (!empty) { + if (value != null && allSourcesFinished) { + queue.offer(combinerSubscriber, latest.clone()); + } else + if (value == null && error.get() != null) { + done = true; // if this source completed without a value } + } else { + done = true; } } + if (!allSourcesFinished && value != null) { + combinerSubscriber.requestMore(1); + return; + } + drain(); } - - public void onCompleted(int index, boolean hadValue) { - if (!hadValue) { - child.onCompleted(); + void drain() { + if (getAndIncrement() != 0) { return; } - boolean done = false; - synchronized (this) { - if (!completion.get(index)) { - completion.set(index); - completionCount++; - done = completionCount == collectedValues.length; + + final Queue q = queue; + final Subscriber a = actual; + final boolean delayError = this.delayError; + final AtomicLong localRequested = this.requested; + + int missed = 1; + for (;;) { + + if (checkTerminated(done, q.isEmpty(), a, q, delayError)) { + return; } - } - if (done) { - buffer.onCompleted(); - tick(); - } - } + + long requestAmount = localRequested.get(); + boolean unbounded = requestAmount == Long.MAX_VALUE; + long emitted = 0L; + + while (requestAmount != 0L) { + + boolean d = done; + @SuppressWarnings("unchecked") + CombinerSubscriber cs = (CombinerSubscriber)q.peek(); + boolean empty = cs == null; + + if (checkTerminated(d, empty, a, q, delayError)) { + return; + } + + if (empty) { + break; + } - /** - * @return boolean true if propagated value - */ - public boolean onNext(int index, T t) { - synchronized (this) { - if (!haveValues.get(index)) { - haveValues.set(index); - haveValuesCount++; + q.poll(); + Object[] array = (Object[])q.poll(); + + if (array == null) { + cancelled = true; + cancel(q); + a.onError(new IllegalStateException("Broken queue?! Sender received but not the array.")); + return; + } + + R v; + try { + v = combiner.call(array); + } catch (Throwable ex) { + cancelled = true; + cancel(q); + a.onError(ex); + return; + } + + a.onNext(v); + + cs.requestMore(1); + + requestAmount--; + emitted--; + } + + if (emitted != 0L) { + if (!unbounded) { + localRequested.addAndGet(emitted); + } } - collectedValues[index] = t; - if (haveValuesCount != collectedValues.length) { - // haven't received value from each source yet so won't emit - return false; + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + + boolean checkTerminated(boolean mainDone, boolean queueEmpty, Subscriber childSubscriber, Queue q, boolean delayError) { + if (cancelled) { + cancel(q); + return true; + } + if (mainDone) { + if (delayError) { + if (queueEmpty) { + Throwable e = error.get(); + if (e != null) { + childSubscriber.onError(e); + } else { + childSubscriber.onCompleted(); + } + return true; + } } else { - try { - buffer.onNext(combinator.call(collectedValues)); - } catch (MissingBackpressureException e) { - onError(e); - } catch (Throwable e) { - Exceptions.throwOrReport(e, child); + Throwable e = error.get(); + if (e != null) { + cancel(q); + childSubscriber.onError(e); + return true; + } else + if (queueEmpty) { + childSubscriber.onCompleted(); + return true; } } } - tick(); - return true; + return false; } - - public void onError(Throwable e) { - child.onError(e); + + void onError(Throwable e) { + AtomicReference localError = this.error; + for (;;) { + Throwable curr = localError.get(); + Throwable next; + if (curr != null) { + if (curr instanceof CompositeException) { + CompositeException ce = (CompositeException) curr; + List es = new ArrayList(ce.getExceptions()); + es.add(e); + next = new CompositeException(es); + } else { + next = new CompositeException(Arrays.asList(curr, e)); + } + } else { + next = e; + } + if (localError.compareAndSet(curr, next)) { + return; + } + } } } - - final static class MultiSourceRequestableSubscriber extends Subscriber { - - final MultiSourceProducer producer; + + static final class CombinerSubscriber extends Subscriber { + final LatestCoordinator parent; final int index; - final AtomicLong emitted = new AtomicLong(); - boolean hasValue = false; - - public MultiSourceRequestableSubscriber(int index, int initial, Subscriber child, MultiSourceProducer producer) { - super(child); + final NotificationLite nl; + + boolean done; + + public CombinerSubscriber(LatestCoordinator parent, int index) { + this.parent = parent; this.index = index; - this.producer = producer; - request(initial); - } - - public void requestUpTo(long n) { - do { - long r = emitted.get(); - long u = Math.min(r, n); - if (emitted.compareAndSet(r, r - u)) { - request(u); - break; - } - } while (true); + this.nl = NotificationLite.instance(); + request(parent.bufferSize); } - - @Override - public void onCompleted() { - producer.onCompleted(index, hasValue); - } - - @Override - public void onError(Throwable e) { - producer.onError(e); - } - + @Override public void onNext(T t) { - hasValue = true; - emitted.incrementAndGet(); - boolean emitted = producer.onNext(index, t); - if (!emitted) { - request(1); + if (done) { + return; } + parent.combine(nl.next(t), index); } - - } - - final static class SingleSourceProducer implements Producer { - final AtomicBoolean started = new AtomicBoolean(); - final Observable source; - final Subscriber child; - final FuncN combinator; - final SingleSourceRequestableSubscriber subscriber; - - public SingleSourceProducer(final Subscriber child, Observable source, FuncN combinator) { - this.source = source; - this.child = child; - this.combinator = combinator; - this.subscriber = new SingleSourceRequestableSubscriber(child, combinator); - } - + @Override - public void request(final long n) { - subscriber.requestMore(n); - if (started.compareAndSet(false, true)) { - source.unsafeSubscribe(subscriber); + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(t); + return; } - + parent.onError(t); + done = true; + parent.combine(null, index); } - - } - - final static class SingleSourceRequestableSubscriber extends Subscriber { - - private final Subscriber child; - private final FuncN combinator; - - SingleSourceRequestableSubscriber(Subscriber child, FuncN combinator) { - super(child); - this.child = child; - this.combinator = combinator; + + @Override + public void onCompleted() { + if (done) { + return; + } + done = true; + parent.combine(null, index); } - + public void requestMore(long n) { request(n); } - - @Override - public void onNext(T t) { - child.onNext(combinator.call(t)); - } - - @Override - public void onError(Throwable e) { - child.onError(e); - } - - @Override - public void onCompleted() { - child.onCompleted(); - } } } diff --git a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java index c28606cae0..a2b8b32763 100644 --- a/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java +++ b/src/test/java/rx/internal/operators/OnSubscribeCombineLatestTest.java @@ -16,40 +16,20 @@ package rx.internal.operators; import static org.junit.Assert.*; -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.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; import org.junit.Test; -import org.mockito.InOrder; -import org.mockito.Matchers; +import org.mockito.*; -import rx.Notification; +import rx.*; import rx.Observable; import rx.Observer; -import rx.Subscriber; -import rx.functions.Action1; -import rx.functions.Func2; -import rx.functions.Func3; -import rx.functions.Func4; -import rx.functions.Func5; -import rx.functions.Func6; -import rx.functions.Func7; -import rx.functions.Func8; -import rx.functions.Func9; -import rx.functions.FuncN; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; @@ -851,6 +831,7 @@ public Long call(Long t1, Integer t2) { @Test(timeout=10000) public void testCombineLatestRequestOverflow() throws InterruptedException { + @SuppressWarnings("unchecked") List> sources = Arrays.asList(Observable.from(Arrays.asList(1,2,3,4)), Observable.from(Arrays.asList(5,6,7,8))); Observable o = Observable.combineLatest(sources,new FuncN() { @Override @@ -884,4 +865,93 @@ public void onNext(Integer t) { assertTrue(latch.await(10, TimeUnit.SECONDS)); } + @Test + public void testCombineMany() { + int n = RxRingBuffer.SIZE * 3; + + List> sources = new ArrayList>(); + + StringBuilder expected = new StringBuilder(n * 2); + + for (int i = 0; i < n; i++) { + sources.add(Observable.just(i)); + expected.append(i); + } + + TestSubscriber ts = TestSubscriber.create(); + + Observable.combineLatest(sources, new FuncN() { + @Override + public String call(Object... args) { + StringBuilder b = new StringBuilder(); + for (Object o : args) { + b.append(o); + } + return b.toString(); + } + }).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValue(expected.toString()); + ts.assertCompleted(); + } + + @Test + public void testCombineManyNulls() { + int n = RxRingBuffer.SIZE * 3; + + Observable source = Observable.just((Integer)null); + + List> sources = new ArrayList>(); + + for (int i = 0; i < n; i++) { + sources.add(source); + } + + TestSubscriber ts = TestSubscriber.create(); + + Observable.combineLatest(sources, new FuncN() { + @Override + public Integer call(Object... args) { + int sum = 0; + for (Object o : args) { + if (o == null) { + sum ++; + } + } + return sum; + } + }).subscribe(ts); + + ts.assertValue(n); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void testNonFatalExceptionThrownByCombinatorForSingleSourceIsNotReportedByUpstreamOperator() { + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + TestSubscriber ts = TestSubscriber.create(1); + Observable source = Observable.just(1) + // if haven't caught exception in combineLatest operator then would incorrectly + // be picked up by this call to doOnError + .doOnError(new Action1() { + @Override + public void call(Throwable t) { + errorOccurred.set(true); + } + }); + Observable + .combineLatest(Collections.singletonList(source), THROW_NON_FATAL) + .subscribe(ts); + assertFalse(errorOccurred.get()); + } + + private static final FuncN THROW_NON_FATAL = new FuncN() { + @Override + public Integer call(Object... args) { + throw new RuntimeException(); + } + + }; } From 13ce7a9ab347161dd44752071d13e643141b3f0a Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 2 Feb 2016 09:43:01 +0100 Subject: [PATCH 500/857] 1.x: fix doOnRequest premature requesting --- .../operators/OperatorDoOnRequest.java | 1 + .../operators/OperatorDoOnRequestTest.java | 67 ++++++++++++++++--- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java index d68c3497aa..419eb7046c 100644 --- a/src/main/java/rx/internal/operators/OperatorDoOnRequest.java +++ b/src/main/java/rx/internal/operators/OperatorDoOnRequest.java @@ -57,6 +57,7 @@ private static final class ParentSubscriber extends Subscriber { ParentSubscriber(Subscriber child) { this.child = child; + this.request(0); } private void requestMore(long n) { diff --git a/src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java b/src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java index 34014094b6..80f5fd4fc8 100644 --- a/src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java +++ b/src/test/java/rx/internal/operators/OperatorDoOnRequestTest.java @@ -1,19 +1,16 @@ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.*; +import java.util.concurrent.atomic.*; -import org.junit.Test; +import org.junit.*; +import rx.*; import rx.Observable; -import rx.Subscriber; -import rx.functions.Action0; -import rx.functions.Action1; +import rx.Observable.OnSubscribe; +import rx.functions.*; public class OperatorDoOnRequestTest { @@ -76,5 +73,55 @@ public void onNext(Integer t) { }); assertEquals(Arrays.asList(3L,1L,2L,3L,4L,5L), requests); } + + @Test + public void dontRequestIfDownstreamRequestsLate() { + final List requested = new ArrayList(); + + Action1 empty = Actions.empty(); + + final AtomicReference producer = new AtomicReference(); + + Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + t.setProducer(new Producer() { + @Override + public void request(long n) { + requested.add(n); + } + }); + } + }).doOnRequest(empty).subscribe(new Subscriber() { + @Override + public void onNext(Object t) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onCompleted() { + + } + + @Override + public void setProducer(Producer p) { + producer.set(p); + } + }); + + producer.get().request(1); + int s = requested.size(); + if (s == 1) { + // this allows for an implementation that itself doesn't request + Assert.assertEquals(Arrays.asList(1L), requested); + } else { + Assert.assertEquals(Arrays.asList(0L, 1L), requested); + } + } } From 4369e1c9e051ee2b42f5dbe2062458303219b382 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 2 Feb 2016 09:53:48 +0100 Subject: [PATCH 501/857] 1.x: fix sample(other) backpressure and unsubscription behavior --- .../OperatorSampleWithObservable.java | 26 ++- .../operators/OperatorSampleTest.java | 154 ++++++++++++++++++ 2 files changed, 172 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java b/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java index 3b3e295dd3..45614dfc28 100644 --- a/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java +++ b/src/main/java/rx/internal/operators/OperatorSampleWithObservable.java @@ -16,9 +16,9 @@ package rx.internal.operators; import java.util.concurrent.atomic.AtomicReference; -import rx.Observable; + +import rx.*; import rx.Observable.Operator; -import rx.Subscriber; import rx.observers.SerializedSubscriber; /** @@ -44,7 +44,9 @@ public Subscriber call(Subscriber child) { final AtomicReference value = new AtomicReference(EMPTY_TOKEN); - Subscriber samplerSub = new Subscriber(child) { + final AtomicReference main = new AtomicReference(); + + final Subscriber samplerSub = new Subscriber() { @Override public void onNext(U t) { Object localValue = value.getAndSet(EMPTY_TOKEN); @@ -58,15 +60,17 @@ public void onNext(U t) { @Override public void onError(Throwable e) { s.onError(e); - unsubscribe(); + // no need to null check, main is assigned before any of the two gets subscribed + main.get().unsubscribe(); } @Override public void onCompleted() { + // onNext(null); // emit the very last value? s.onCompleted(); - unsubscribe(); + // no need to null check, main is assigned before any of the two gets subscribed + main.get().unsubscribe(); } - }; Subscriber result = new Subscriber() { @@ -78,17 +82,23 @@ public void onNext(T t) { @Override public void onError(Throwable e) { s.onError(e); - unsubscribe(); + + samplerSub.unsubscribe(); } @Override public void onCompleted() { + // samplerSub.onNext(null); // emit the very last value? s.onCompleted(); - unsubscribe(); + + samplerSub.unsubscribe(); } }; + main.lazySet(result); + child.add(result); + child.add(samplerSub); sampler.unsafeSubscribe(samplerSub); diff --git a/src/test/java/rx/internal/operators/OperatorSampleTest.java b/src/test/java/rx/internal/operators/OperatorSampleTest.java index 1db795cbfb..78d3633d6f 100644 --- a/src/test/java/rx/internal/operators/OperatorSampleTest.java +++ b/src/test/java/rx/internal/operators/OperatorSampleTest.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.*; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; import org.junit.*; import org.mockito.InOrder; @@ -26,8 +27,10 @@ import rx.*; import rx.Observable.OnSubscribe; import rx.functions.*; +import rx.observers.TestSubscriber; import rx.schedulers.TestScheduler; import rx.subjects.PublishSubject; +import rx.subscriptions.Subscriptions; public class OperatorSampleTest { private TestScheduler scheduler; @@ -312,4 +315,155 @@ public void call(Long t) { Assert.assertEquals(Long.MAX_VALUE, requested[0]); } + + @Test + public void dontUnsubscribeChild1() { + TestSubscriber ts = new TestSubscriber(); + + PublishSubject source = PublishSubject.create(); + + PublishSubject sampler = PublishSubject.create(); + + source.sample(sampler).unsafeSubscribe(ts); + + source.onCompleted(); + + Assert.assertFalse("Source has subscribers?", source.hasObservers()); + Assert.assertFalse("Sampler has subscribers?", sampler.hasObservers()); + + Assert.assertFalse("TS unsubscribed?", ts.isUnsubscribed()); + } + + @Test + public void dontUnsubscribeChild2() { + TestSubscriber ts = new TestSubscriber(); + + PublishSubject source = PublishSubject.create(); + + PublishSubject sampler = PublishSubject.create(); + + source.sample(sampler).unsafeSubscribe(ts); + + sampler.onCompleted(); + + Assert.assertFalse("Source has subscribers?", source.hasObservers()); + Assert.assertFalse("Sampler has subscribers?", sampler.hasObservers()); + + Assert.assertFalse("TS unsubscribed?", ts.isUnsubscribed()); + } + + @Test + public void neverSetProducer() { + Observable neverBackpressure = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + t.setProducer(new Producer() { + @Override + public void request(long n) { + // irrelevant in this test + } + }); + } + }); + + final AtomicInteger count = new AtomicInteger(); + + neverBackpressure.sample(neverBackpressure).unsafeSubscribe(new Subscriber() { + @Override + public void onNext(Integer t) { + // irrelevant + } + + @Override + public void onError(Throwable e) { + // irrelevant + } + + @Override + public void onCompleted() { + // irrelevant + } + + @Override + public void setProducer(Producer p) { + count.incrementAndGet(); + } + }); + + Assert.assertEquals(0, count.get()); + } + + @Test + public void unsubscribeMainAfterCompleted() { + final AtomicBoolean unsubscribed = new AtomicBoolean(); + + Observable source = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + t.add(Subscriptions.create(new Action0() { + @Override + public void call() { + unsubscribed.set(true); + } + })); + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onCompleted() { + if (unsubscribed.get()) { + onError(new IllegalStateException("Resource unsubscribed!")); + } else { + super.onCompleted(); + } + } + }; + + PublishSubject sampler = PublishSubject.create(); + + source.sample(sampler).unsafeSubscribe(ts); + + sampler.onCompleted(); + + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void unsubscribeSamplerAfterCompleted() { + final AtomicBoolean unsubscribed = new AtomicBoolean(); + + Observable source = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber t) { + t.add(Subscriptions.create(new Action0() { + @Override + public void call() { + unsubscribed.set(true); + } + })); + } + }); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onCompleted() { + if (unsubscribed.get()) { + onError(new IllegalStateException("Resource unsubscribed!")); + } else { + super.onCompleted(); + } + } + }; + + PublishSubject sampled = PublishSubject.create(); + + sampled.sample(source).unsafeSubscribe(ts); + + sampled.onCompleted(); + + ts.assertNoErrors(); + ts.assertCompleted(); + } } From 4373f7540d50c5a029df9070dcae1be872f87a42 Mon Sep 17 00:00:00 2001 From: mushuichuan Date: Mon, 18 Jan 2016 10:48:25 +0800 Subject: [PATCH 502/857] add optimization to just method in Single --- src/main/java/rx/Single.java | 26 +- .../util/ScalarSynchronousSingle.java | 157 ++++++++++ src/test/java/rx/SingleTest.java | 6 +- .../util/ScalarSynchronousSingleTest.java | 285 ++++++++++++++++++ 4 files changed, 461 insertions(+), 13 deletions(-) create mode 100644 src/main/java/rx/internal/util/ScalarSynchronousSingle.java create mode 100644 src/test/java/rx/internal/util/ScalarSynchronousSingleTest.java diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 5ffbee393b..4f72edddf7 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -37,6 +37,8 @@ import rx.annotations.Beta; import rx.internal.operators.*; import rx.internal.producers.SingleDelayedProducer; +import rx.internal.util.ScalarSynchronousSingle; +import rx.internal.util.UtilityFunctions; import rx.singles.BlockingSingle; import rx.observers.SafeSubscriber; import rx.plugins.*; @@ -654,15 +656,7 @@ public void call(SingleSubscriber singleSubscriber) { * @see ReactiveX operators documentation: Just */ public static Single just(final T value) { - // TODO add similar optimization as ScalarSynchronousObservable - return Single.create(new OnSubscribe() { - - @Override - public void call(SingleSubscriber te) { - te.onSuccess(value); - } - - }); + return ScalarSynchronousSingle.create(value); } /** @@ -683,6 +677,9 @@ public void call(SingleSubscriber te) { * @see ReactiveX operators documentation: Merge */ public static Single merge(final Single> source) { + if (source instanceof ScalarSynchronousSingle) { + return ((ScalarSynchronousSingle) source).scalarFlatMap((Func1) UtilityFunctions.identity()); + } return Single.create(new OnSubscribe() { @Override @@ -1296,6 +1293,9 @@ public final Observable concatWith(Single t1) { * @see ReactiveX operators documentation: FlatMap */ public final Single flatMap(final Func1> func) { + if (this instanceof ScalarSynchronousSingle) { + return ((ScalarSynchronousSingle) this).scalarFlatMap(func); + } return merge(map(func)); } @@ -1378,6 +1378,9 @@ public final Observable mergeWith(Single t1) { * @see #subscribeOn */ public final Single observeOn(Scheduler scheduler) { + if (this instanceof ScalarSynchronousSingle) { + return ((ScalarSynchronousSingle)this).scalarScheduleOn(scheduler); + } return lift(new OperatorObserveOn(scheduler)); } @@ -1737,6 +1740,9 @@ public void onNext(T t) { * @see #observeOn */ public final Single subscribeOn(final Scheduler scheduler) { + if (this instanceof ScalarSynchronousSingle) { + return ((ScalarSynchronousSingle)this).scalarScheduleOn(scheduler); + } return create(new OnSubscribe() { @Override public void call(final SingleSubscriber t) { @@ -1772,7 +1778,7 @@ public void onError(Throwable error) { } }); } - }); + }); } /** diff --git a/src/main/java/rx/internal/util/ScalarSynchronousSingle.java b/src/main/java/rx/internal/util/ScalarSynchronousSingle.java new file mode 100644 index 0000000000..83b7d456a1 --- /dev/null +++ b/src/main/java/rx/internal/util/ScalarSynchronousSingle.java @@ -0,0 +1,157 @@ +/** + * 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.util; + +import rx.Scheduler; +import rx.Scheduler.Worker; +import rx.Single; +import rx.SingleSubscriber; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Func1; +import rx.internal.schedulers.EventLoopsScheduler; + +public final class ScalarSynchronousSingle extends Single { + + public static final ScalarSynchronousSingle create(T t) { + return new ScalarSynchronousSingle(t); + } + + final T value; + + protected ScalarSynchronousSingle(final T t) { + super(new OnSubscribe() { + + @Override + public void call(SingleSubscriber te) { + te.onSuccess(t); + } + + }); + this.value = t; + } + + public T get() { + return value; + } + + /** + * Customized observeOn/subscribeOn implementation which emits the scalar + * value directly or with less overhead on the specified scheduler. + * + * @param scheduler the target scheduler + * @return the new observable + */ + public Single scalarScheduleOn(Scheduler scheduler) { + if (scheduler instanceof EventLoopsScheduler) { + EventLoopsScheduler es = (EventLoopsScheduler) scheduler; + return create(new DirectScheduledEmission(es, value)); + } + return create(new NormalScheduledEmission(scheduler, value)); + } + + /** + * Optimized observeOn for scalar value observed on the EventLoopsScheduler. + */ + static final class DirectScheduledEmission implements OnSubscribe { + private final EventLoopsScheduler es; + private final T value; + + DirectScheduledEmission(EventLoopsScheduler es, T value) { + this.es = es; + this.value = value; + } + + @Override + public void call(SingleSubscriber singleSubscriber) { + singleSubscriber.add(es.scheduleDirect(new ScalarSynchronousSingleAction(singleSubscriber, value))); + } + } + + /** + * Emits a scalar value on a general scheduler. + */ + static final class NormalScheduledEmission implements OnSubscribe { + private final Scheduler scheduler; + private final T value; + + NormalScheduledEmission(Scheduler scheduler, T value) { + this.scheduler = scheduler; + this.value = value; + } + + @Override + public void call(SingleSubscriber singleSubscriber) { + Worker worker = scheduler.createWorker(); + singleSubscriber.add(worker); + worker.schedule(new ScalarSynchronousSingleAction(singleSubscriber, value)); + } + } + + /** + * Action that emits a single value when called. + */ + static final class ScalarSynchronousSingleAction implements Action0 { + private final SingleSubscriber subscriber; + private final T value; + + ScalarSynchronousSingleAction(SingleSubscriber subscriber, + T value) { + this.subscriber = subscriber; + this.value = value; + } + + @Override + public void call() { + try { + subscriber.onSuccess(value); + } catch (Throwable t) { + subscriber.onError(t); + } + } + } + + public Single scalarFlatMap(final Func1> func) { + return create(new OnSubscribe() { + @Override + public void call(final SingleSubscriber child) { + + Single o = func.call(value); + if (o instanceof ScalarSynchronousSingle) { + child.onSuccess(((ScalarSynchronousSingle) o).value); + } else { + Subscriber subscriber = new Subscriber() { + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(R r) { + child.onSuccess(r); + } + }; + child.add(subscriber); + o.unsafeSubscribe(subscriber); + } + } + }); + } +} diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index b29fcb01af..488a2c0d52 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -1,11 +1,11 @@ /** * Copyright 2015 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. diff --git a/src/test/java/rx/internal/util/ScalarSynchronousSingleTest.java b/src/test/java/rx/internal/util/ScalarSynchronousSingleTest.java new file mode 100644 index 0000000000..61700af4d1 --- /dev/null +++ b/src/test/java/rx/internal/util/ScalarSynchronousSingleTest.java @@ -0,0 +1,285 @@ +/** + * 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.util; + +import org.junit.Test; + +import rx.Single; +import rx.SingleSubscriber; +import rx.Subscription; +import rx.exceptions.TestException; +import rx.functions.Action0; +import rx.functions.Func1; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; +import rx.subscriptions.Subscriptions; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class ScalarSynchronousSingleTest { + @Test + public void backPressure() { + TestSubscriber ts = TestSubscriber.create(0); + + Single.just(1).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void backPressureSubscribeOn() throws Exception { + TestSubscriber ts = TestSubscriber.create(0); + + Single.just(1).subscribeOn(Schedulers.computation()).subscribe(ts); + + Thread.sleep(200); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.awaitTerminalEvent(); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void backPressureObserveOn() throws Exception { + TestSubscriber ts = TestSubscriber.create(0); + + Single.just(1).observeOn(Schedulers.computation()).subscribe(ts); + + Thread.sleep(200); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.awaitTerminalEvent(); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void backPressureSubscribeOn2() throws Exception { + TestSubscriber ts = TestSubscriber.create(0); + + Single.just(1).subscribeOn(Schedulers.newThread()).subscribe(ts); + + Thread.sleep(200); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.awaitTerminalEvent(); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void backPressureObserveOn2() throws Exception { + TestSubscriber ts = TestSubscriber.create(0); + + Single.just(1).observeOn(Schedulers.newThread()).subscribe(ts); + + Thread.sleep(200); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.awaitTerminalEvent(); + + ts.assertValue(1); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void backPressureFlatMapJust() { + TestSubscriber ts = TestSubscriber.create(0); + + Single.just(1).flatMap(new Func1>() { + @Override + public Single call(Integer v) { + return Single.just(String.valueOf(v)); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValue("1"); + ts.assertCompleted(); + ts.assertNoErrors(); + + ts.requestMore(1); + + ts.assertValue("1"); + ts.assertCompleted(); + ts.assertNoErrors(); + } + + @Test + public void syncObserverNextThrows() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + Single.just(1).unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void syncFlatMapJustObserverNextThrows() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + Single.just(1) + .flatMap(new Func1>() { + @Override + public Single call(Integer v) { + return Single.just(v); + } + }) + .unsafeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test(timeout = 1000) + public void asyncObserverNextThrows() { + TestSubscriber ts = new TestSubscriber() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + Single.just(1).subscribeOn(Schedulers.computation()).unsafeSubscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + + @Test + public void scalarFlatMap() { + final Action0 unSubscribe = mock(Action0.class); + Single s = Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber subscriber) { + subscriber.add(Subscriptions.create(unSubscribe)); + } + }); + Subscription subscription = Single.merge(Single.just(s)).subscribe(); + subscription.unsubscribe(); + verify(unSubscribe).call(); + } + + @Test + public void scalarFlatMapError() { + final Throwable error = new IllegalStateException(); + Single s = Single.just(1); + TestSubscriber testSubscriber = new TestSubscriber(); + s.flatMap(new Func1>() { + @Override + public Single call(Integer integer) { + return Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + singleSubscriber.onError(error); + } + }); + } + }).subscribe(testSubscriber); + testSubscriber.assertNoValues(); + testSubscriber.assertError(error); + } + + @Test + public void scalarFlatMapSuccess() { + Single s = Single.just(1); + TestSubscriber testSubscriber = new TestSubscriber(); + s.flatMap(new Func1>() { + @Override + public Single call(final Integer integer) { + return Single.create(new Single.OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + singleSubscriber.onSuccess(String.valueOf(integer)); + } + }); + } + }).subscribe(testSubscriber); + testSubscriber.assertNoErrors(); + testSubscriber.assertValue("1"); + } + + @Test + public void getValue() { + Single s = Single.just(1); + assertEquals(1, ((ScalarSynchronousSingle) s).get()); + } +} \ No newline at end of file From 80ccf8451291dbbdbb6f7800d6b605cb4bd4b3f5 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Tue, 2 Feb 2016 20:51:10 -0500 Subject: [PATCH 503/857] Update javadoc for Subscribers.empty() --- src/main/java/rx/observers/Subscribers.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/rx/observers/Subscribers.java b/src/main/java/rx/observers/Subscribers.java index 4e81c1af8d..b6240ffd61 100644 --- a/src/main/java/rx/observers/Subscribers.java +++ b/src/main/java/rx/observers/Subscribers.java @@ -31,10 +31,9 @@ private Subscribers() { } /** - * Returns an inert {@link Subscriber} that does nothing in response to the emissions or notifications from - * any {@code Observable} it subscribes to. This is different, however, from an {@code EmptyObserver}, in - * that it will throw an exception if its {@link Subscriber#onError onError} method is called (whereas - * {@code EmptyObserver} will swallow the error in such a case). + * Returns an inert {@link Subscriber} that does nothing in response to the emissions or notifications + * from any {@code Observable} it subscribes to. Will throw an exception if {@link Subscriber#onError onError} + * method is called * * @return an inert {@code Observer} */ From 349f940189ace9a3f6fd392bf7f5ca7e3ecef921 Mon Sep 17 00:00:00 2001 From: Mike Nakhimovich Date: Wed, 3 Feb 2016 07:40:45 -0500 Subject: [PATCH 504/857] Update Subscribers.java --- src/main/java/rx/observers/Subscribers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/observers/Subscribers.java b/src/main/java/rx/observers/Subscribers.java index b6240ffd61..6c82bfb82e 100644 --- a/src/main/java/rx/observers/Subscribers.java +++ b/src/main/java/rx/observers/Subscribers.java @@ -32,7 +32,7 @@ private Subscribers() { /** * Returns an inert {@link Subscriber} that does nothing in response to the emissions or notifications - * from any {@code Observable} it subscribes to. Will throw an exception if {@link Subscriber#onError onError} + * from any {@code Observable} it subscribes to. Will throw an {@link OnErrorNotImplementedException} if {@link Subscriber#onError onError} * method is called * * @return an inert {@code Observer} From 234a4c4672da9f26492c48f48abe03c0573c1b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Wed, 3 Feb 2016 22:53:02 +0100 Subject: [PATCH 505/857] 1.x: concat reduce overhead when streaming a source --- .../internal/operators/BackpressureUtils.java | 23 ++++++ .../rx/internal/operators/OperatorConcat.java | 55 +++++++++----- src/perf/java/rx/operators/ConcatPerf.java | 75 +++++++++++++++++++ 3 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 src/perf/java/rx/operators/ConcatPerf.java diff --git a/src/main/java/rx/internal/operators/BackpressureUtils.java b/src/main/java/rx/internal/operators/BackpressureUtils.java index 937f186535..0d4adef0a8 100644 --- a/src/main/java/rx/internal/operators/BackpressureUtils.java +++ b/src/main/java/rx/internal/operators/BackpressureUtils.java @@ -103,4 +103,27 @@ public static long addCap(long a, long b) { return u; } + /** + * Atomically subtracts a value from the requested amount unless it's at Long.MAX_VALUE. + * @param requested the requested amount holder + * @param n the value to subtract from the requested amount, has to be positive (not verified) + * @return the new requested amount + * @throws IllegalStateException if n is greater than the current requested amount, which + * indicates a bug in the request accounting logic + */ + public static long produced(AtomicLong requested, long n) { + for (;;) { + long current = requested.get(); + if (current == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long next = current - n; + if (next < 0L) { + throw new IllegalStateException("More produced than requested: " + next); + } + if (requested.compareAndSet(current, next)) { + return next; + } + } + } } diff --git a/src/main/java/rx/internal/operators/OperatorConcat.java b/src/main/java/rx/internal/operators/OperatorConcat.java index 8455cc55b3..e251841f18 100644 --- a/src/main/java/rx/internal/operators/OperatorConcat.java +++ b/src/main/java/rx/internal/operators/OperatorConcat.java @@ -16,18 +16,14 @@ package rx.internal.operators; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.*; -import rx.Observable; +import rx.*; import rx.Observable.Operator; -import rx.Producer; -import rx.Subscriber; import rx.functions.Action0; import rx.internal.producers.ProducerArbiter; import rx.observers.SerializedSubscriber; -import rx.subscriptions.SerialSubscription; -import rx.subscriptions.Subscriptions; +import rx.subscriptions.*; /** * Returns an Observable that emits the items emitted by two or more Observables, one after the other. @@ -112,9 +108,19 @@ public void onStart() { } private void requestFromChild(long n) { - if (n <=0) return; + if (n <= 0) return; // we track 'requested' so we know whether we should subscribe the next or not - long previous = BackpressureUtils.getAndAddRequest(requested, n); + + 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) { @@ -125,10 +131,6 @@ private void requestFromChild(long n) { } } - private void decrementRequested() { - requested.decrementAndGet(); - } - @Override public void onNext(Observable t) { queue.add(nl.next(t)); @@ -167,8 +169,10 @@ void subscribeNext() { child.onCompleted(); } else if (o != null) { Observable obs = nl.getValue(o); + currentSubscriber = new ConcatInnerSubscriber(this, child, arbiter); current.set(currentSubscriber); + obs.unsafeSubscribe(currentSubscriber); } } else { @@ -179,14 +183,23 @@ void subscribeNext() { } } } + + 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 AtomicInteger once = new AtomicInteger(); + private final AtomicBoolean once = new AtomicBoolean(); private final ProducerArbiter arbiter; + + long produced; public ConcatInnerSubscriber(ConcatSubscriber parent, Subscriber child, ProducerArbiter arbiter) { this.parent = parent; @@ -196,14 +209,14 @@ public ConcatInnerSubscriber(ConcatSubscriber parent, Subscriber child, Pr @Override public void onNext(T t) { + produced++; + child.onNext(t); - parent.decrementRequested(); - arbiter.produced(1); } @Override public void onError(Throwable e) { - if (once.compareAndSet(0, 1)) { + if (once.compareAndSet(false, true)) { // terminal error through parent so everything gets cleaned up, including this inner parent.onError(e); } @@ -211,9 +224,12 @@ public void onError(Throwable e) { @Override public void onCompleted() { - if (once.compareAndSet(0, 1)) { + 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 - parent.completeInner(); + p.completeInner(); } } @@ -221,6 +237,5 @@ public void onCompleted() { public void setProducer(Producer producer) { arbiter.setProducer(producer); } - } } diff --git a/src/perf/java/rx/operators/ConcatPerf.java b/src/perf/java/rx/operators/ConcatPerf.java new file mode 100644 index 0000000000..c9c5e8e18f --- /dev/null +++ b/src/perf/java/rx/operators/ConcatPerf.java @@ -0,0 +1,75 @@ +/** + * 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.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.jmh.LatchedObserver; + +/** + * Benchmark typical atomic operations on volatile fields and AtomicXYZ classes. + *

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

    + * gradlew benchmarks "-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*ConcatPerf.*" + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class ConcatPerf { + + Observable source; + + Observable baseline; + + @Param({"1", "1000", "1000000"}) + int count; + + @Setup + public void setup() { + Integer[] array = new Integer[count]; + + for (int i = 0; i < count; i++) { + array[i] = 777; + } + + baseline = Observable.from(array); + + source = Observable.concat(baseline, Observable.empty()); + } + + @Benchmark + public void normal(Blackhole bh) { + source.subscribe(new LatchedObserver(bh)); + } + + @Benchmark + public void baseline(Blackhole bh) { + baseline.subscribe(new LatchedObserver(bh)); + } +} From 6a572ecd4081d0635d5aa634e2606fdd3d607cff Mon Sep 17 00:00:00 2001 From: Oguz Babaoglu Date: Thu, 4 Feb 2016 15:07:22 +0100 Subject: [PATCH 506/857] 1.x: fix Subscribers.create(onNext) javadoc as per #3669 --- src/main/java/rx/observers/Subscribers.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/observers/Subscribers.java b/src/main/java/rx/observers/Subscribers.java index 6cf85a6935..274165181a 100644 --- a/src/main/java/rx/observers/Subscribers.java +++ b/src/main/java/rx/observers/Subscribers.java @@ -68,8 +68,8 @@ public void onNext(T t) { /** * Creates a {@link Subscriber} that receives the emissions of any {@code Observable} it subscribes to via - * {@link Subscriber#onNext onNext} but ignores {@link Subscriber#onError onError} and - * {@link Subscriber#onCompleted onCompleted} notifications. + * {@link Subscriber#onNext onNext} but ignores {@link Subscriber#onCompleted onCompleted} notifications; + * it will throw an {@link OnErrorNotImplementedException} if {@link Subscriber#onError onError} is invoked. * * @param onNext * a function that handles each item emitted by an {@code Observable} From e0a2bcbe3caa5d8a64ddee2f179fec5e44cb9dc7 Mon Sep 17 00:00:00 2001 From: Harun Urhan Date: Fri, 5 Feb 2016 12:11:34 +0200 Subject: [PATCH 507/857] Add How to Contribute wiki link --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 425985b8a1..090f0e8257 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,8 @@ If you would like to contribute code you can do so through GitHub by forking the When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. +[See How To Contribute wiki page for more details](https://github.com/ReactiveX/RxJava/wiki/How-to-Contribute) + ## License By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/ReactiveX/RxJava/blob/master/LICENSE From e8059c652acbec0de6607678e9cd5bcb28bc2d5f Mon Sep 17 00:00:00 2001 From: akarnokd Date: Sat, 6 Feb 2016 09:18:14 +0100 Subject: [PATCH 508/857] 1.x: change take(negative) to throw IAE. --- .../rx/internal/operators/OperatorTake.java | 3 +++ .../rx/internal/operators/OperatorTakeTest.java | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/rx/internal/operators/OperatorTake.java b/src/main/java/rx/internal/operators/OperatorTake.java index d1cc1cbd09..55b0288f12 100644 --- a/src/main/java/rx/internal/operators/OperatorTake.java +++ b/src/main/java/rx/internal/operators/OperatorTake.java @@ -36,6 +36,9 @@ public final class OperatorTake implements Operator { final int limit; public OperatorTake(int limit) { + if (limit < 0) { + throw new IllegalArgumentException("limit >= 0 required but it was " + limit); + } this.limit = limit; } diff --git a/src/test/java/rx/internal/operators/OperatorTakeTest.java b/src/test/java/rx/internal/operators/OperatorTakeTest.java index 4173f08892..df23a64150 100644 --- a/src/test/java/rx/internal/operators/OperatorTakeTest.java +++ b/src/test/java/rx/internal/operators/OperatorTakeTest.java @@ -438,4 +438,21 @@ public void call(Integer v) { ts.assertNoErrors(); ts.assertCompleted(); } + + @Test(expected = IllegalArgumentException.class) + public void takeNegative() { + Observable.range(1, 1000 * 1000 * 1000).take(-1); + } + + @Test(timeout = 1000) + public void takeZero() { + TestSubscriber ts = TestSubscriber.create(); + + Observable.range(1, 1000 * 1000 * 1000).take(0).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertCompleted(); + } + } From ddaafc3b4f51c26649c12ecedd053f3f616bdaca Mon Sep 17 00:00:00 2001 From: Shixiong Zhu Date: Sun, 7 Feb 2016 18:48:44 -0800 Subject: [PATCH 509/857] 1.x: negative argument check for skip's count and merge's maxConcurrent --- .../rx/internal/operators/OperatorMerge.java | 3 +++ .../rx/internal/operators/OperatorSkip.java | 3 +++ .../internal/operators/OperatorMergeTest.java | 20 +++++++++++++++++++ .../internal/operators/OperatorSkipTest.java | 18 +++++++---------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMerge.java b/src/main/java/rx/internal/operators/OperatorMerge.java index 56a7058d26..835ccd7695 100644 --- a/src/main/java/rx/internal/operators/OperatorMerge.java +++ b/src/main/java/rx/internal/operators/OperatorMerge.java @@ -81,6 +81,9 @@ public static OperatorMerge instance(boolean delayErrors) { * @return */ public static OperatorMerge instance(boolean delayErrors, int maxConcurrent) { + if (maxConcurrent <= 0) { + throw new IllegalArgumentException("maxConcurrent > 0 required but it was " + maxConcurrent); + } if (maxConcurrent == Integer.MAX_VALUE) { return instance(delayErrors); } diff --git a/src/main/java/rx/internal/operators/OperatorSkip.java b/src/main/java/rx/internal/operators/OperatorSkip.java index 505c1491e7..bc63fe5e21 100644 --- a/src/main/java/rx/internal/operators/OperatorSkip.java +++ b/src/main/java/rx/internal/operators/OperatorSkip.java @@ -31,6 +31,9 @@ public final class OperatorSkip implements Observable.Operator { final int toSkip; public OperatorSkip(int n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } this.toSkip = n; } diff --git a/src/test/java/rx/internal/operators/OperatorMergeTest.java b/src/test/java/rx/internal/operators/OperatorMergeTest.java index 78cf229bbc..2c40ac53d3 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeTest.java @@ -1333,4 +1333,24 @@ public void testConcurrencyLimit() { ts.assertValue(0); ts.assertCompleted(); } + + @Test + public void negativeMaxConcurrent() { + try { + Observable.merge(Arrays.asList(Observable.just(1), Observable.just(2)), -1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("maxConcurrent > 0 required but it was -1", e.getMessage()); + } + } + + @Test + public void zeroMaxConcurrent() { + try { + Observable.merge(Arrays.asList(Observable.just(1), Observable.just(2)), 0); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("maxConcurrent > 0 required but it was 0", e.getMessage()); + } + } } diff --git a/src/test/java/rx/internal/operators/OperatorSkipTest.java b/src/test/java/rx/internal/operators/OperatorSkipTest.java index 0e9ca9367e..21ad07b251 100644 --- a/src/test/java/rx/internal/operators/OperatorSkipTest.java +++ b/src/test/java/rx/internal/operators/OperatorSkipTest.java @@ -16,6 +16,7 @@ package rx.internal.operators; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -37,17 +38,12 @@ public class OperatorSkipTest { @Test public void testSkipNegativeElements() { - - Observable skip = Observable.just("one", "two", "three").lift(new OperatorSkip(-99)); - - @SuppressWarnings("unchecked") - Observer observer = mock(Observer.class); - skip.subscribe(observer); - verify(observer, times(1)).onNext("one"); - verify(observer, times(1)).onNext("two"); - verify(observer, times(1)).onNext("three"); - verify(observer, never()).onError(any(Throwable.class)); - verify(observer, times(1)).onCompleted(); + try { + Observable.just("one", "two", "three").skip(-99); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("n >= 0 required but it was -99", e.getMessage()); + } } @Test From d179699fef37985f5d4528a8d6f4edd26d227b93 Mon Sep 17 00:00:00 2001 From: JonWowUs Date: Wed, 20 Jan 2016 16:11:27 +0000 Subject: [PATCH 510/857] Added MergeDelay operators for Iterable of Observables --- src/main/java/rx/Observable.java | 59 +++++++++++++++++++ .../OperatorMergeDelayErrorTest.java | 19 +++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 9287e105de..534342b24e 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -2186,6 +2186,65 @@ public final static Observable mergeDelayError(Observableinstance(true, maxConcurrent)); } + /** + * Flattens an Iterable of Observables into one Observable, in a way that allows an Observer to receive all + * successfully emitted items from each of the source Observables without being interrupted by an error + * notification from one of them. + *

    + * This behaves like {@link #merge(Observable)} except that if any of the merged Observables notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged Observables have finished emitting items. + *

    + * + *

    + * Even if multiple merged Observables send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its Observers once. + *

    + *
    Scheduler:
    + *
    {@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param sequences + * the Iterable of Observables + * @return an Observable that emits items that are the result of flattening the items emitted by the + * Observables in the Iterable + * @see ReactiveX operators documentation: Merge + */ + public static Observable mergeDelayError(Iterable> sequences) { + return mergeDelayError(from(sequences)); + } + + /** + * Flattens an Iterable of Observables into one Observable, in a way that allows an Observer to receive all + * successfully emitted items from each of the source Observables without being interrupted by an error + * notification from one of them, while limiting the number of concurrent subscriptions to these Observables. + *

    + * This behaves like {@link #merge(Observable)} except that if any of the merged Observables notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged Observables have finished emitting items. + *

    + * + *

    + * Even if multiple merged Observables send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its Observers once. + *

    + *
    Scheduler:
    + *
    {@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param sequences + * the Iterable of Observables + * @param maxConcurrent + * the maximum number of Observables that may be subscribed to concurrently + * @return an Observable that emits items that are the result of flattening the items emitted by the + * Observables in the Iterable + * @see ReactiveX operators documentation: Merge + */ + public static Observable mergeDelayError(Iterable> sequences, int maxConcurrent) { + return mergeDelayError(from(sequences), maxConcurrent); + } + + /** * Flattens two Observables into one Observable, in a way that allows an Observer to receive all * successfully emitted items from each of the source Observables without being interrupted by an error diff --git a/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java b/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java index db086d6632..e5088a0a4a 100644 --- a/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java +++ b/src/test/java/rx/internal/operators/OperatorMergeDelayErrorTest.java @@ -272,6 +272,23 @@ public void testMergeList() { verify(stringObserver, times(2)).onNext("hello"); } + // This is pretty much a clone of testMergeList but with the overloaded MergeDelayError for Iterables + @Test + public void mergeIterable() { + final Observable o1 = Observable.create(new TestSynchronousObservable()); + final Observable o2 = Observable.create(new TestSynchronousObservable()); + List> listOfObservables = new ArrayList>(); + listOfObservables.add(o1); + listOfObservables.add(o2); + + Observable m = Observable.mergeDelayError(listOfObservables); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onCompleted(); + verify(stringObserver, times(2)).onNext("hello"); + } + @Test public void testMergeArrayWithThreading() { final TestASynchronousObservable o1 = new TestASynchronousObservable(); @@ -577,4 +594,4 @@ public void call(Long t1) { assertTrue(ts.getOnErrorEvents().get(0) instanceof TestException); assertEquals(Arrays.asList(1L, 1L, 1L), requests); } -} \ No newline at end of file +} From 9aef7cb1e590019d75a6faa7dac0677d1b40afb3 Mon Sep 17 00:00:00 2001 From: Steve Gury Date: Mon, 8 Feb 2016 15:29:42 -0800 Subject: [PATCH 511/857] Fix various misspellings in OperatorPublish No code change. --- .../rx/internal/operators/OperatorPublish.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 65cf83dd25..2714b81ffb 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -81,7 +81,7 @@ public void call(Subscriber child) { */ continue; /* - * Note: although technically corrent, concurrent disconnects can cause + * Note: although technically correct, concurrent disconnects can cause * unexpected behavior such as child subscribers never receiving anything * (unless connected again). An alternative approach, similar to * PublishSubject would be to immediately terminate such child @@ -309,7 +309,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 } } @@ -398,7 +398,7 @@ boolean checkTerminated(Object term, boolean empty) { ip.child.onCompleted(); } } finally { - // we explicitely unsubscribe/disconnect from the upstream + // we explicitly unsubscribe/disconnect from the upstream // after we sent out the terminal event to child subscribers unsubscribe(); } @@ -418,7 +418,7 @@ boolean checkTerminated(Object term, boolean empty) { ip.child.onError(t); } } finally { - // we explicitely unsubscribe/disconnect from the upstream + // we explicitly unsubscribe/disconnect from the upstream // after we sent out the terminal event to child subscribers unsubscribe(); } @@ -637,7 +637,7 @@ static final class InnerProducer extends AtomicLong implements Producer, Subs /** * Indicates this child has not yet requested any value. We pretend we don't * see such child subscribers in dispatch() to allow other child subscribers who - * have requested to make progress. In a concurrent subscription scennario, + * have requested to make progress. In a concurrent subscription scenario, * one can't be sure when a subscription happens exactly so this virtual shift * should not cause any problems. */ @@ -685,7 +685,7 @@ public void request(long n) { } // try setting the new request value if (compareAndSet(r, u)) { - // if successful, notify the parent dispacher this child can receive more + // if successful, notify the parent dispatcher this child can receive more // elements parent.dispatch(); return; @@ -725,7 +725,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 From 2367f90bb0f3bce5493ac5e014b599133c4410a7 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 9 Feb 2016 12:52:30 +0100 Subject: [PATCH 512/857] 1.x: fix observeOn resource handling, add delayError capability --- src/main/java/rx/Observable.java | 37 ++- src/main/java/rx/Single.java | 4 +- .../internal/operators/OperatorObserveOn.java | 237 ++++++++++-------- .../util/atomic/SpscAtomicArrayQueue.java | 5 + .../internal/util/unsafe/SpscArrayQueue.java | 5 + .../operators/OperatorObserveOnTest.java | 93 ++++--- 6 files changed, 240 insertions(+), 141 deletions(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7a2d91a2af..b0c5e3b935 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -5999,7 +5999,9 @@ public final Observable mergeWith(Observable t1) { /** * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, - * asynchronously with an unbounded buffer. + * asynchronously with a bounded buffer. + *

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

    * *

    @@ -6014,12 +6016,43 @@ public final Observable mergeWith(Observable t1) { * @see ReactiveX operators documentation: ObserveOn * @see RxJava Threading Examples * @see #subscribeOn + * @see #observeOn(Scheduler, boolean) */ public final Observable observeOn(Scheduler scheduler) { if (this instanceof ScalarSynchronousObservable) { return ((ScalarSynchronousObservable)this).scalarScheduleOn(scheduler); } - return lift(new OperatorObserveOn(scheduler)); + return lift(new OperatorObserveOn(scheduler, false)); + } + + /** + * Modifies an Observable to perform its emissions and notifications on a specified {@link Scheduler}, + * asynchronously with a bounded buffer 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 + * @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) + */ + public final Observable observeOn(Scheduler scheduler, boolean delayError) { + if (this instanceof ScalarSynchronousObservable) { + return ((ScalarSynchronousObservable)this).scalarScheduleOn(scheduler); + } + return lift(new OperatorObserveOn(scheduler, delayError)); } /** diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index a768779a4d..5ad3d92f73 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1381,7 +1381,9 @@ public final Single observeOn(Scheduler scheduler) { if (this instanceof ScalarSynchronousSingle) { return ((ScalarSynchronousSingle)this).scalarScheduleOn(scheduler); } - return lift(new OperatorObserveOn(scheduler)); + // Note that since Single emits onSuccess xor onError, + // there is no cut-ahead possible like with regular Observable sequences. + return lift(new OperatorObserveOn(scheduler, false)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorObserveOn.java b/src/main/java/rx/internal/operators/OperatorObserveOn.java index 8aff74e67f..98464efb89 100644 --- a/src/main/java/rx/internal/operators/OperatorObserveOn.java +++ b/src/main/java/rx/internal/operators/OperatorObserveOn.java @@ -16,22 +16,17 @@ package rx.internal.operators; import java.util.Queue; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import rx.*; import rx.Observable.Operator; -import rx.Producer; -import rx.Scheduler; -import rx.Subscriber; -import rx.Subscription; import rx.exceptions.MissingBackpressureException; import rx.functions.Action0; -import rx.internal.util.RxRingBuffer; -import rx.internal.util.SynchronizedQueue; -import rx.internal.util.unsafe.SpscArrayQueue; -import rx.internal.util.unsafe.UnsafeAccess; -import rx.schedulers.ImmediateScheduler; -import rx.schedulers.TrampolineScheduler; +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. @@ -44,12 +39,15 @@ public final class OperatorObserveOn implements Operator { private final Scheduler scheduler; + private final boolean delayError; /** - * @param scheduler + * @param scheduler the scheduler to use + * @param delayError delay errors until all normal events are emitted in the other thread? */ - public OperatorObserveOn(Scheduler scheduler) { + public OperatorObserveOn(Scheduler scheduler, boolean delayError) { this.scheduler = scheduler; + this.delayError = delayError; } @Override @@ -61,58 +59,65 @@ public Subscriber call(Subscriber child) { // avoid overhead, execute directly return child; } else { - ObserveOnSubscriber parent = new ObserveOnSubscriber(scheduler, child); + ObserveOnSubscriber parent = new ObserveOnSubscriber(scheduler, child, delayError); parent.init(); return parent; } } /** Observe through individual queue per observer. */ - private static final class ObserveOnSubscriber extends Subscriber { + private static final class ObserveOnSubscriber extends Subscriber implements Action0 { final Subscriber child; final Scheduler.Worker recursiveScheduler; - final ScheduledUnsubscribe scheduledUnsubscribe; - final NotificationLite on = NotificationLite.instance(); - + final NotificationLite on; + final boolean delayError; final Queue queue; // the status of the current stream - volatile boolean finished = false; + volatile boolean finished; final AtomicLong requested = new AtomicLong(); final AtomicLong counter = new AtomicLong(); - volatile Throwable error; + /** + * The single exception if not null, should be written before setting finished (release) and read after + * reading finished (acquire). + */ + Throwable error; // 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) { + public ObserveOnSubscriber(Scheduler scheduler, Subscriber child, boolean delayError) { this.child = child; this.recursiveScheduler = scheduler.createWorker(); + this.delayError = delayError; + this.on = NotificationLite.instance(); if (UnsafeAccess.isUnsafeAvailable()) { queue = new SpscArrayQueue(RxRingBuffer.SIZE); } else { - queue = new SynchronizedQueue(RxRingBuffer.SIZE); + queue = new SpscAtomicArrayQueue(RxRingBuffer.SIZE); } - this.scheduledUnsubscribe = new ScheduledUnsubscribe(recursiveScheduler); } void init() { // don't want this code in the constructor because `this` can escape through the // setProducer call - child.add(scheduledUnsubscribe); - child.setProducer(new Producer() { + Subscriber localChild = child; + + localChild.setProducer(new Producer() { @Override public void request(long n) { - BackpressureUtils.getAndAddRequest(requested, n); - schedule(); + if (n > 0L) { + BackpressureUtils.getAndAddRequest(requested, n); + schedule(); + } } }); - child.add(recursiveScheduler); - child.add(this); + localChild.add(recursiveScheduler); + localChild.add(this); } @Override @@ -123,7 +128,7 @@ public void onStart() { @Override public void onNext(final T t) { - if (isUnsubscribed()) { + if (isUnsubscribed() || finished) { return; } if (!queue.offer(on.next(t))) { @@ -145,106 +150,126 @@ public void onCompleted() { @Override public void onError(final Throwable e) { if (isUnsubscribed() || finished) { + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); return; } error = e; - // unsubscribe eagerly since time will pass before the scheduled onError results in an unsubscribe event - unsubscribe(); finished = true; - // polling thread should skip any onNext still in the queue schedule(); } - final Action0 action = new Action0() { - - @Override - public void call() { - pollQueue(); - } - - }; - protected void schedule() { if (counter.getAndIncrement() == 0) { - recursiveScheduler.schedule(action); + recursiveScheduler.schedule(this); } } // only execute this from schedule() - void pollQueue() { - int emitted = 0; - final AtomicLong localRequested = this.requested; - final AtomicLong localCounter = this.counter; - do { - localCounter.set(1); - long produced = 0; - long r = localRequested.get(); - for (;;) { - if (child.isUnsubscribed()) + @Override + public void call() { + long emitted = 0L; + + long missed = 1L; + + // these are accessed in a tight loop around atomics so + // loading them into local variables avoids the mandatory re-reading + // of the constant fields + final Queue q = this.queue; + final Subscriber localChild = this.child; + final NotificationLite localOn = this.on; + + // 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) + + 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) { + boolean done = finished; + Object v = q.poll(); + boolean empty = v == null; + + if (checkTerminated(done, empty, localChild, q)) { return; - Throwable error; - if (finished) { - if ((error = this.error) != null) { - // errors shortcut the queue so - // release the elements in the queue for gc - queue.clear(); - child.onError(error); - return; - } else - if (queue.isEmpty()) { - child.onCompleted(); - return; - } } - if (r > 0) { - Object o = queue.poll(); - if (o != null) { - child.onNext(on.getValue(o)); - r--; - emitted++; - produced++; - } else { - break; - } - } else { + + if (empty) { break; } + + localChild.onNext(localOn.getValue(v)); + + requestAmount--; + currentEmission--; + emitted++; + } + + if (currentEmission != 0L && !unbounded) { + requested.addAndGet(currentEmission); } - if (produced > 0 && localRequested.get() != Long.MAX_VALUE) { - localRequested.addAndGet(-produced); + + missed = counter.addAndGet(-missed); + if (missed == 0L) { + break; } - } while (localCounter.decrementAndGet() > 0); - if (emitted > 0) { + } + + if (emitted != 0L) { request(emitted); } } - } - - static final class ScheduledUnsubscribe extends AtomicInteger implements Subscription { - final Scheduler.Worker worker; - volatile boolean unsubscribed = false; - - public ScheduledUnsubscribe(Scheduler.Worker worker) { - this.worker = worker; - } - - @Override - public boolean isUnsubscribed() { - return unsubscribed; - } - - @Override - public void unsubscribe() { - if (getAndSet(1) == 0) { - worker.schedule(new Action0() { - @Override - public void call() { - worker.unsubscribe(); - unsubscribed = true; + + boolean checkTerminated(boolean done, boolean isEmpty, Subscriber a, Queue q) { + if (a.isUnsubscribed()) { + q.clear(); + return true; + } + + if (done) { + if (delayError) { + if (isEmpty) { + Throwable e = error; + try { + if (e != null) { + a.onError(e); + } else { + a.onCompleted(); + } + } finally { + recursiveScheduler.unsubscribe(); + } + } + } else { + Throwable e = error; + if (e != null) { + q.clear(); + try { + a.onError(e); + } finally { + recursiveScheduler.unsubscribe(); + } + return true; + } else + if (isEmpty) { + try { + a.onCompleted(); + } finally { + recursiveScheduler.unsubscribe(); + } + return true; } - }); + } + } + + return false; } - } } diff --git a/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java b/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java index 65c29e3ce8..cadf772d49 100644 --- a/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java +++ b/src/main/java/rx/internal/util/atomic/SpscAtomicArrayQueue.java @@ -107,6 +107,11 @@ public int size() { } } + @Override + public boolean isEmpty() { + return lvProducerIndex() == lvConsumerIndex(); + } + private void soProducerIndex(long newIndex) { producerIndex.lazySet(newIndex); } diff --git a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java index 88c6d491c6..17fee1c804 100644 --- a/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java +++ b/src/main/java/rx/internal/util/unsafe/SpscArrayQueue.java @@ -162,6 +162,11 @@ public int size() { } } } + + @Override + public boolean isEmpty() { + return lvProducerIndex() == lvConsumerIndex(); + } private void soProducerIndex(long v) { UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, v); diff --git a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java index 65a4085384..0b4b98bc8e 100644 --- a/src/test/java/rx/internal/operators/OperatorObserveOnTest.java +++ b/src/test/java/rx/internal/operators/OperatorObserveOnTest.java @@ -15,47 +15,26 @@ */ package rx.internal.operators; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -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.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; +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.*; import org.junit.Test; import org.mockito.InOrder; -import rx.Notification; +import rx.*; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observer; -import rx.Scheduler; -import rx.Subscriber; -import rx.Subscription; -import rx.exceptions.MissingBackpressureException; -import rx.exceptions.TestException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.functions.Func2; +import rx.exceptions.*; +import rx.functions.*; import rx.internal.util.RxRingBuffer; import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.schedulers.TestScheduler; +import rx.schedulers.*; import rx.subjects.PublishSubject; public class OperatorObserveOnTest { @@ -804,5 +783,55 @@ public void onNext(Integer t) { assertTrue(latch.await(10, TimeUnit.SECONDS)); assertEquals(1, requests.size()); } + + @Test + public void testErrorDelayed() { + TestScheduler s = Schedulers.test(); + + Observable source = Observable.just(1, 2 ,3) + .concatWith(Observable.error(new TestException())); + + TestSubscriber ts = TestSubscriber.create(0); + + source.observeOn(s, true).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + s.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + s.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(3); // requesting 2 doesn't switch to the error() source for some reason in concat. + s.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValues(1, 2, 3); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } + @Test + public void testErrorDelayedAsync() { + Observable source = Observable.just(1, 2 ,3) + .concatWith(Observable.error(new TestException())); + + TestSubscriber ts = TestSubscriber.create(); + + source.observeOn(Schedulers.computation(), true).subscribe(ts); + + ts.awaitTerminalEvent(2, TimeUnit.SECONDS); + ts.assertValues(1, 2, 3); + ts.assertError(TestException.class); + ts.assertNotCompleted(); + } } From c3a23c58ac71128f0e3e51732e4dcf6ded31907c Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 9 Feb 2016 13:28:44 +0100 Subject: [PATCH 513/857] 1.x: javadoc for rx.exceptions.Exceptions See #1508 --- src/main/java/rx/exceptions/Exceptions.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/rx/exceptions/Exceptions.java b/src/main/java/rx/exceptions/Exceptions.java index dea54a9b26..081e4830a8 100644 --- a/src/main/java/rx/exceptions/Exceptions.java +++ b/src/main/java/rx/exceptions/Exceptions.java @@ -21,11 +21,13 @@ import rx.annotations.Experimental; /** - * @warn javadoc class description missing + * Utility class with methods to wrap checked exceptions and + * manage fatal and regular exception delivery. */ public final class Exceptions { + /** Utility class, no instances. */ private Exceptions() { - + throw new IllegalStateException("No instances!"); } /** From d827eb4acfe267e3964b077a7f42cd7d082a6de1 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 9 Feb 2016 13:39:17 +0100 Subject: [PATCH 514/857] 1.x: javadoc for Producer --- src/main/java/rx/Producer.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Producer.java b/src/main/java/rx/Producer.java index 4d9f4428fd..9bb9cc22d1 100644 --- a/src/main/java/rx/Producer.java +++ b/src/main/java/rx/Producer.java @@ -16,7 +16,16 @@ package rx; /** - * @warn javadoc description missing + * Interface that establishes a request-channel between an Observable and a Subscriber and allows + * the Subscriber to request a certain amount of items from the Observable (otherwise known as + * backpressure). + * + *

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

    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 + * in unbounded mode. Depending on the chain of operators, this can lead to {@link rx.exceptions.MissingBackpressureException}. */ public interface Producer { @@ -36,6 +45,7 @@ public interface Producer { * * @param n the maximum number of items you want this Producer to produce, or {@code Long.MAX_VALUE} if you * want the Producer to produce items at its own pace + * @throws IllegalArgumentException if the request amount is negative */ void request(long n); From 99d5c60df87e3160e12925557cf83c4959022474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Tue, 9 Feb 2016 19:23:54 +0100 Subject: [PATCH 515/857] 1.x: unified onErrorX and onExceptionResumeNext and fixed backpressure --- src/main/java/rx/Observable.java | 6 +- src/main/java/rx/Single.java | 2 +- .../OperatorOnErrorResumeNextViaFunction.java | 50 +++++++- ...peratorOnErrorResumeNextViaObservable.java | 104 ---------------- .../operators/OperatorOnErrorReturn.java | 111 ----------------- ...torOnExceptionResumeNextViaObservable.java | 113 ------------------ ...ratorOnErrorResumeNextViaFunctionTest.java | 33 +++++ ...torOnErrorResumeNextViaObservableTest.java | 28 +++++ .../operators/OperatorOnErrorReturnTest.java | 33 ++++- ...nExceptionResumeNextViaObservableTest.java | 37 ++++-- 10 files changed, 172 insertions(+), 345 deletions(-) delete mode 100644 src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservable.java delete mode 100644 src/main/java/rx/internal/operators/OperatorOnErrorReturn.java delete mode 100644 src/main/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservable.java diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 7a2d91a2af..73dd40eb67 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -6230,7 +6230,7 @@ public final Observable onErrorResumeNext(final Func1ReactiveX operators documentation: Catch */ public final Observable onErrorResumeNext(final Observable resumeSequence) { - return lift(new OperatorOnErrorResumeNextViaObservable(resumeSequence)); + return lift(OperatorOnErrorResumeNextViaFunction.withOther(resumeSequence)); } /** @@ -6260,7 +6260,7 @@ public final Observable onErrorResumeNext(final Observable resum * @see ReactiveX operators documentation: Catch */ public final Observable onErrorReturn(Func1 resumeFunction) { - return lift(new OperatorOnErrorReturn(resumeFunction)); + return lift(OperatorOnErrorResumeNextViaFunction.withSingle(resumeFunction)); } /** @@ -6296,7 +6296,7 @@ public final Observable onErrorReturn(Func1 resumeFun * @see ReactiveX operators documentation: Catch */ public final Observable onExceptionResumeNext(final Observable resumeSequence) { - return lift(new OperatorOnExceptionResumeNextViaObservable(resumeSequence)); + return lift(OperatorOnErrorResumeNextViaFunction.withException(resumeSequence)); } /** diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index a768779a4d..dfab031332 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1411,7 +1411,7 @@ public final Single observeOn(Scheduler scheduler) { * @see ReactiveX operators documentation: Catch */ public final Single onErrorReturn(Func1 resumeFunction) { - return lift(new OperatorOnErrorReturn(resumeFunction)); + return lift(OperatorOnErrorResumeNextViaFunction.withSingle(resumeFunction)); } /** diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java index b12c10d391..48a03ea30b 100644 --- a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java +++ b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunction.java @@ -45,6 +45,36 @@ public final class OperatorOnErrorResumeNextViaFunction implements Operator> resumeFunction; + public static OperatorOnErrorResumeNextViaFunction withSingle(final Func1 resumeFunction) { + return new OperatorOnErrorResumeNextViaFunction(new Func1>() { + @Override + public Observable call(Throwable t) { + return Observable.just(resumeFunction.call(t)); + } + }); + } + + public static OperatorOnErrorResumeNextViaFunction withOther(final Observable other) { + return new OperatorOnErrorResumeNextViaFunction(new Func1>() { + @Override + public Observable call(Throwable t) { + return other; + } + }); + } + + public static OperatorOnErrorResumeNextViaFunction withException(final Observable other) { + return new OperatorOnErrorResumeNextViaFunction(new Func1>() { + @Override + public Observable call(Throwable t) { + if (t instanceof Exception) { + return other; + } + return Observable.error(t); + } + }); + } + public OperatorOnErrorResumeNextViaFunction(Func1> f) { this.resumeFunction = f; } @@ -52,10 +82,14 @@ public OperatorOnErrorResumeNextViaFunction(Func1 call(final Subscriber child) { final ProducerArbiter pa = new ProducerArbiter(); + final SerialSubscription ssub = new SerialSubscription(); + Subscriber parent = new Subscriber() { - private boolean done = false; + private boolean done; + + long produced; @Override public void onCompleted() { @@ -70,12 +104,13 @@ public void onCompleted() { public void onError(Throwable e) { if (done) { Exceptions.throwIfFatal(e); + RxJavaPlugins.getInstance().getErrorHandler().handleError(e); return; } done = true; try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); unsubscribe(); + Subscriber next = new Subscriber() { @Override public void onNext(T t) { @@ -96,7 +131,13 @@ public void setProducer(Producer producer) { }; ssub.set(next); + long p = produced; + if (p != 0L) { + pa.produced(p); + } + Observable resume = resumeFunction.call(e); + resume.unsafeSubscribe(next); } catch (Throwable e2) { Exceptions.throwOrReport(e2, child); @@ -108,6 +149,7 @@ public void onNext(T t) { if (done) { return; } + produced++; child.onNext(t); } @@ -117,9 +159,11 @@ public void setProducer(final Producer producer) { } }; - child.add(ssub); ssub.set(parent); + + child.add(ssub); child.setProducer(pa); + return parent; } diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservable.java b/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservable.java deleted file mode 100644 index 3e8afcea00..0000000000 --- a/src/main/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservable.java +++ /dev/null @@ -1,104 +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 rx.Observable; -import rx.Producer; -import rx.Observable.Operator; -import rx.Subscriber; -import rx.exceptions.Exceptions; -import rx.plugins.RxJavaPlugins; - -/** - * Instruct an Observable to pass control to another Observable rather than invoking - * onError if it encounters an error. - *

    - * - *

    - * By default, when an Observable encounters an error that prevents it from emitting the expected item to its - * Observer, the Observable invokes its Observer's {@code onError} method, and then quits without invoking any - * more of its Observer's methods. The {@code onErrorResumeNext} operation changes this behavior. If you pass - * an Observable ({@code resumeSequence}) to {@code onErrorResumeNext}, if the source Observable encounters an - * error, instead of invoking its Observer's {@code onError} method, it will instead relinquish control to this - * new Observable, which will invoke the Observer's {@code onNext} method if it is able to do so. In such a - * case, because no Observable 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. - * - * @param the value type - */ -public final class OperatorOnErrorResumeNextViaObservable implements Operator { - final Observable resumeSequence; - - public OperatorOnErrorResumeNextViaObservable(Observable resumeSequence) { - this.resumeSequence = resumeSequence; - } - - @Override - public Subscriber call(final Subscriber child) { - // shared subscription won't work here - Subscriber s = new Subscriber() { - - private boolean done = false; - - @Override - public void onNext(T t) { - if (done) { - return; - } - child.onNext(t); - } - - @Override - public void onError(Throwable e) { - if (done) { - Exceptions.throwIfFatal(e); - return; - } - done = true; - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); - unsubscribe(); - resumeSequence.unsafeSubscribe(child); - } - - @Override - public void onCompleted() { - if (done) { - return; - } - done = true; - child.onCompleted(); - } - - @Override - public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - @Override - public void request(long n) { - producer.request(n); - } - }); - } - - }; - child.add(s); - - return s; - } - -} diff --git a/src/main/java/rx/internal/operators/OperatorOnErrorReturn.java b/src/main/java/rx/internal/operators/OperatorOnErrorReturn.java deleted file mode 100644 index 3830f591fd..0000000000 --- a/src/main/java/rx/internal/operators/OperatorOnErrorReturn.java +++ /dev/null @@ -1,111 +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.Arrays; - -import rx.Observable.Operator; -import rx.Producer; -import rx.Subscriber; -import rx.exceptions.CompositeException; -import rx.exceptions.Exceptions; -import rx.functions.Func1; -import rx.plugins.RxJavaPlugins; - -/** - * Instruct an Observable to emit a particular item to its Observer's onNext method - * rather than invoking onError if it encounters an error. - *

    - * - *

    - * By default, when an Observable encounters an error that prevents it from emitting the expected - * item to its Observer, the Observable invokes its Observer's onError method, and then - * quits without invoking any more of its Observer's methods. The onErrorReturn operation changes - * this behavior. If you pass a function (resumeFunction) to onErrorReturn, if the original - * Observable encounters an error, instead of invoking its Observer's onError method, - * it will instead pass the return value of resumeFunction to the Observer's onNext - * method. - *

    - * You can use this to prevent errors from propagating or to supply fallback data should errors be - * encountered. - * - * @param the value type - */ -public final class OperatorOnErrorReturn implements Operator { - final Func1 resultFunction; - - public OperatorOnErrorReturn(Func1 resultFunction) { - this.resultFunction = resultFunction; - } - - @Override - public Subscriber call(final Subscriber child) { - Subscriber parent = new Subscriber() { - - private boolean done = false; - - @Override - public void onNext(T t) { - if (done) { - return; - } - child.onNext(t); - } - - @Override - public void onError(Throwable e) { - if (done) { - Exceptions.throwIfFatal(e); - return; - } - done = true; - try { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); - unsubscribe(); - T result = resultFunction.call(e); - child.onNext(result); - } catch (Throwable x) { - Exceptions.throwIfFatal(x); - child.onError(new CompositeException(Arrays.asList(e, x))); - return; - } - child.onCompleted(); - } - - @Override - public void onCompleted() { - if (done) { - return; - } - done = true; - child.onCompleted(); - } - - @Override - public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - @Override - public void request(long n) { - producer.request(n); - } - }); - } - - }; - child.add(parent); - return parent; - } -} diff --git a/src/main/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservable.java b/src/main/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservable.java deleted file mode 100644 index be76097443..0000000000 --- a/src/main/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservable.java +++ /dev/null @@ -1,113 +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 rx.Observable; -import rx.Producer; -import rx.Observable.Operator; -import rx.Subscriber; -import rx.exceptions.Exceptions; -import rx.plugins.RxJavaPlugins; - -/** - * Instruct an Observable to pass control to another Observable rather than invoking - * onError if it encounters an error of type {@link java.lang.Exception}. - *

    - * This differs from {@link Observable#onErrorResumeNext} in that this one does not handle - * {@link java.lang.Throwable} or {@link java.lang.Error} but lets those continue through. - *

    - * - *

    - * By default, when an Observable encounters an error that prevents it from emitting the expected - * item to its Observer, the Observable invokes its Observer's onError method, and - * then quits without invoking any more of its Observer's methods. The onErrorResumeNext operation - * changes this behavior. If you pass an Observable (resumeSequence) to onErrorResumeNext, if the - * source Observable encounters an error, instead of invoking its Observer's onError - * method, it will instead relinquish control to this new Observable, which will invoke the - * Observer's onNext method if it is able to do so. In such a case, because no - * Observable necessarily invokes 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. - * - * @param the value type - */ -public final class OperatorOnExceptionResumeNextViaObservable implements Operator { - final Observable resumeSequence; - - public OperatorOnExceptionResumeNextViaObservable(Observable resumeSequence) { - this.resumeSequence = resumeSequence; - } - - @Override - public Subscriber call(final Subscriber child) { - // needs to independently unsubscribe so child can continue with the resume - Subscriber s = new Subscriber() { - - private boolean done = false; - - @Override - public void onNext(T t) { - if (done) { - return; - } - child.onNext(t); - } - - @Override - public void onError(Throwable e) { - if (done) { - Exceptions.throwIfFatal(e); - return; - } - done = true; - if (e instanceof Exception) { - RxJavaPlugins.getInstance().getErrorHandler().handleError(e); - unsubscribe(); - resumeSequence.unsafeSubscribe(child); - } else { - child.onError(e); - } - } - - @Override - public void onCompleted() { - if (done) { - return; - } - done = true; - child.onCompleted(); - } - - @Override - public void setProducer(final Producer producer) { - child.setProducer(new Producer() { - @Override - public void request(long n) { - producer.request(n); - } - }); - } - - }; - child.add(s); - - return s; - } - - -} diff --git a/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunctionTest.java b/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunctionTest.java index 1aab90867d..a7cee6966f 100644 --- a/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunctionTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaFunctionTest.java @@ -30,12 +30,14 @@ import rx.Observable; import rx.Observable.Operator; +import rx.exceptions.TestException; import rx.Observer; import rx.Subscriber; import rx.Subscription; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorOnErrorResumeNextViaFunctionTest { @@ -344,4 +346,35 @@ public Integer call(Integer t1) { ts.awaitTerminalEvent(); ts.assertNoErrors(); } + + @Test + public void normalBackpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + PublishSubject ps = PublishSubject.create(); + + ps.onErrorResumeNext(new Func1>() { + @Override + public Observable call(Throwable v) { + return Observable.range(3, 2); + } + }).subscribe(ts); + + ts.requestMore(2); + + ps.onNext(1); + ps.onNext(2); + ps.onError(new TestException("Forced failure")); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, 2, 3, 4); + ts.assertNoErrors(); + ts.assertCompleted(); + } + } diff --git a/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java b/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java index 586c2b689d..d67e1d3814 100644 --- a/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnErrorResumeNextViaObservableTest.java @@ -26,12 +26,14 @@ import rx.Observable; import rx.Observable.OnSubscribe; +import rx.exceptions.TestException; import rx.Observer; import rx.Subscriber; import rx.Subscription; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorOnErrorResumeNextViaObservableTest { @@ -221,4 +223,30 @@ public Integer call(Integer t1) { ts.awaitTerminalEvent(); ts.assertNoErrors(); } + + @Test + public void normalBackpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + PublishSubject ps = PublishSubject.create(); + + ps.onErrorResumeNext(Observable.range(3, 2)).subscribe(ts); + + ts.requestMore(2); + + ps.onNext(1); + ps.onNext(2); + ps.onError(new TestException("Forced failure")); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, 2, 3, 4); + ts.assertNoErrors(); + ts.assertCompleted(); + } + } diff --git a/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java b/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java index f74d5d93f4..4124d8d344 100644 --- a/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnErrorReturnTest.java @@ -30,9 +30,11 @@ import rx.Observable; import rx.Observer; import rx.Subscriber; +import rx.exceptions.TestException; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorOnErrorReturnTest { @@ -217,6 +219,33 @@ public void run() { } } - - + @Test + public void normalBackpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + PublishSubject ps = PublishSubject.create(); + + ps.onErrorReturn(new Func1() { + @Override + public Integer call(Throwable e) { + return 3; + } + }).subscribe(ts); + + ts.requestMore(2); + + ps.onNext(1); + ps.onNext(2); + ps.onError(new TestException("Forced failure")); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertCompleted(); + } } diff --git a/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java b/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java index b447a7ab23..2ac3e6eadb 100644 --- a/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java +++ b/src/test/java/rx/internal/operators/OperatorOnExceptionResumeNextViaObservableTest.java @@ -17,21 +17,17 @@ import static org.junit.Assert.fail; import static org.mockito.Matchers.any; -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 static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; import org.junit.Test; import org.mockito.Mockito; -import rx.Observable; -import rx.Observer; -import rx.Subscriber; +import rx.*; +import rx.exceptions.TestException; import rx.functions.Func1; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; public class OperatorOnExceptionResumeNextViaObservableTest { @@ -265,4 +261,29 @@ else if ("THROWABLE".equals(s)) System.out.println("done starting TestObservable thread"); } } + + @Test + public void normalBackpressure() { + TestSubscriber ts = TestSubscriber.create(0); + + PublishSubject ps = PublishSubject.create(); + + ps.onExceptionResumeNext(Observable.range(3, 2)).subscribe(ts); + + ts.requestMore(2); + + ps.onNext(1); + ps.onNext(2); + ps.onError(new TestException("Forced failure")); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(2); + + ts.assertValues(1, 2, 3, 4); + ts.assertNoErrors(); + ts.assertCompleted(); + } } From 6291fb99b80b0887eb8e354b14d8bfc400b35231 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 10 Feb 2016 09:15:16 +0100 Subject: [PATCH 516/857] 1.x: Fix zip() - subscriber array becoming visible too early and causing NPE --- .../rx/internal/operators/OperatorZip.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorZip.java b/src/main/java/rx/internal/operators/OperatorZip.java index 91fc05f09f..6f1280b3c3 100644 --- a/src/main/java/rx/internal/operators/OperatorZip.java +++ b/src/main/java/rx/internal/operators/OperatorZip.java @@ -177,7 +177,10 @@ public void request(long n) { } - private static final class Zip extends AtomicLong { + static final class Zip extends AtomicLong { + /** */ + private static final long serialVersionUID = 5995274816189928317L; + final Observer child; private final FuncN zipFunction; private final CompositeSubscription childSubscription = new CompositeSubscription(); @@ -186,7 +189,7 @@ private static final class Zip extends AtomicLong { int emitted = 0; // not volatile/synchronized as accessed inside COUNTER_UPDATER block /* initialized when started in `start` */ - private Object[] observers; + private volatile Object[] subscribers; private AtomicLong requested; public Zip(final Subscriber child, FuncN zipFunction) { @@ -197,16 +200,18 @@ public Zip(final Subscriber child, FuncN zipFunction) { @SuppressWarnings("unchecked") public void start(@SuppressWarnings("rawtypes") Observable[] os, AtomicLong requested) { - observers = new Object[os.length]; - this.requested = requested; + final Object[] subscribers = new Object[os.length]; for (int i = 0; i < os.length; i++) { InnerSubscriber io = new InnerSubscriber(); - observers[i] = io; + subscribers[i] = io; childSubscription.add(io); } - + + this.requested = requested; + this.subscribers = subscribers; // full memory barrier: release all above + for (int i = 0; i < os.length; i++) { - os[i].unsafeSubscribe((InnerSubscriber) observers[i]); + os[i].unsafeSubscribe((InnerSubscriber) subscribers[i]); } } @@ -219,13 +224,13 @@ public void start(@SuppressWarnings("rawtypes") Observable[] os, AtomicLong requ */ @SuppressWarnings("unchecked") void tick() { - final Object[] observers = this.observers; - if (observers == null) { + final Object[] subscribers = this.subscribers; + if (subscribers == null) { // nothing yet to do (initial request from Producer) return; } if (getAndIncrement() == 0) { - final int length = observers.length; + final int length = subscribers.length; final Observer child = this.child; final AtomicLong requested = this.requested; do { @@ -234,7 +239,7 @@ void tick() { final Object[] vs = new Object[length]; boolean allHaveValues = true; for (int i = 0; i < length; i++) { - RxRingBuffer buffer = ((InnerSubscriber) observers[i]).items; + RxRingBuffer buffer = ((InnerSubscriber) subscribers[i]).items; Object n = buffer.peek(); if (n == null) { @@ -265,7 +270,7 @@ void tick() { return; } // now remove them - for (Object obj : observers) { + for (Object obj : subscribers) { RxRingBuffer buffer = ((InnerSubscriber) obj).items; buffer.poll(); // eagerly check if the next item on this queue is an onComplete @@ -278,7 +283,7 @@ void tick() { } } if (emitted > THRESHOLD) { - for (Object obj : observers) { + for (Object obj : subscribers) { ((InnerSubscriber) obj).requestMore(emitted); } emitted = 0; From 801c707df92b91f9f3f716a04a1bf740b7fc1dee Mon Sep 17 00:00:00 2001 From: Klemen Kresnik Date: Tue, 9 Feb 2016 14:47:58 +0100 Subject: [PATCH 517/857] Added retry and retryWhen support for Single. --- src/main/java/rx/Single.java | 128 +++++++++++++++--- src/test/java/rx/SingleTest.java | 225 ++++++++++++++++++++++--------- 2 files changed, 271 insertions(+), 82 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index dfab031332..6ce380a274 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -12,39 +12,26 @@ */ package rx; -import java.util.Collection; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - import rx.Observable.Operator; +import rx.annotations.Beta; import rx.annotations.Experimental; import rx.exceptions.Exceptions; import rx.exceptions.OnErrorNotImplementedException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.functions.Func2; -import rx.functions.Func3; -import rx.functions.Func4; -import rx.functions.Func5; -import rx.functions.Func6; -import rx.functions.Func7; -import rx.functions.Func8; -import rx.functions.Func9; -import rx.functions.FuncN; -import rx.annotations.Beta; +import rx.functions.*; import rx.internal.operators.*; import rx.internal.producers.SingleDelayedProducer; import rx.internal.util.ScalarSynchronousSingle; import rx.internal.util.UtilityFunctions; -import rx.singles.BlockingSingle; import rx.observers.SafeSubscriber; -import rx.plugins.*; +import rx.plugins.RxJavaObservableExecutionHook; +import rx.plugins.RxJavaPlugins; import rx.schedulers.Schedulers; +import rx.singles.BlockingSingle; import rx.subscriptions.Subscriptions; +import java.util.Collection; +import java.util.concurrent.*; + /** * The Single class implements the Reactive Pattern for a single value response. See {@link Observable} for the * implementation of the Reactive Pattern for a stream or vector of values. @@ -1820,7 +1807,7 @@ public void onError(Throwable error) { * @return an {@link Observable} that emits a single item T. */ public final Observable toObservable() { - return asObservable(this); + return asObservable(this); } /** @@ -2209,4 +2196,101 @@ static Single[] iterableToArray(final Iterable + * + * If the source Single calls {@link SingleSubscriber#onError}, this method will resubscribe to the source + * Single rather than propagating the {@code onError} call. + * + *

    + *
    Scheduler:
    + *
    {@code retry} operates by default on the {@code trampoline} {@link Scheduler}.
    + *
    + * + * @return the source Single modified with retry logic + * @see ReactiveX operators documentation: Retry + */ + public final Single retry() { + return toObservable().retry().toSingle(); + } + + /** + * Returns an Single that mirrors the source Single, resubscribing to it if it calls {@code onError} + * up to a specified number of retries. + * + * + * + * If the source Single calls {@link SingleSubscriber#onError}, this method will resubscribe to the source + * Single for a maximum of {@code count} resubscriptions rather than propagating the + * {@code onError} call. + * + *
    + *
    Scheduler:
    + *
    {@code retry} operates by default on the {@code trampoline} {@link Scheduler}.
    + *
    + * + * @param count + * number of retry attempts before failing + * + * @return the source Single modified with retry logic + * @see ReactiveX operators documentation: Retry + */ + public final Single retry(final long count) { + return toObservable().retry(count).toSingle(); + } + + /** + * Returns an Single that mirrors the source Single, resubscribing to it if it calls {@code onError} + * and the predicate returns true for that specific exception and retry count. + * + * + *
    + *
    Backpressure Support:
    + *
    This operator honors backpressure. + *
    Scheduler:
    + *
    {@code retry} operates by default on the {@code trampoline} {@link Scheduler}.
    + *
    + * + * @param predicate + * the predicate that determines if a resubscription may happen in case of a specific exception + * and retry count + * + * @return the source Single modified with retry logic + * @see #retry() + * @see ReactiveX operators documentation: Retry + */ + public final Single retry(Func2 predicate) { + return toObservable().retry(predicate).toSingle(); + } + + /** + * Returns a Single that emits the same values as the source Single with the exception of an + * {@code onError}. An {@code onError} notification from the source will result in the emission of a + * {@link Throwable} item to the Observable provided as an argument to the {@code notificationHandler} + * function. If that Observable calls {@code onComplete} or {@code onError} then {@code retry} will call + * {@code onCompleted} or {@code onError} on the child subscription. Otherwise, this Observable will + * resubscribe to the source Single. + * + * + * + *
    + *
    Scheduler:
    + *
    {@code retryWhen} operates by default on the {@code trampoline} {@link Scheduler}.
    + *
    + * + * @param notificationHandler + * receives an Observable of notifications with which a user can complete or error, aborting the + * retry + * + * @return the source Single modified with retry logic + * @see ReactiveX operators documentation: Retry + */ + public final Single retryWhen(final Func1, ? extends Observable> notificationHandler) { + return toObservable().retryWhen(notificationHandler).toSingle(); + } + } diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 15de891636..393088562c 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -12,56 +12,27 @@ */ package rx; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - import org.junit.Test; - import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import rx.Single.OnSubscribe; import rx.exceptions.CompositeException; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.functions.Func2; -import rx.functions.Func3; -import rx.functions.Func4; -import rx.functions.Func5; -import rx.functions.Func6; -import rx.functions.Func7; -import rx.functions.Func8; -import rx.functions.Func9; -import rx.functions.FuncN; -import rx.schedulers.TestScheduler; -import rx.singles.BlockingSingle; +import rx.functions.*; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; +import rx.schedulers.TestScheduler; +import rx.singles.BlockingSingle; import rx.subscriptions.Subscriptions; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; + public class SingleTest { @Test @@ -721,11 +692,11 @@ public void onStart() { @Test public void testToObservable() { - Observable a = Single.just("a").toObservable(); - TestSubscriber ts = TestSubscriber.create(); - a.subscribe(ts); - ts.assertValue("a"); - ts.assertCompleted(); + Observable a = Single.just("a").toObservable(); + TestSubscriber ts = TestSubscriber.create(); + a.subscribe(ts); + ts.assertValue("a"); + ts.assertCompleted(); } @Test @@ -1014,7 +985,7 @@ public void deferShouldCallSingleFactoryForEachSubscriber() throws Exception { Callable> singleFactory = mock(Callable.class); String[] values = {"1", "2", "3"}; - final Single[] singles = new Single[]{Single.just(values[0]), Single.just(values[1]), Single.just(values[2])}; + final Single[] singles = new Single[] {Single.just(values[0]), Single.just(values[1]), Single.just(values[2])}; final AtomicInteger singleFactoryCallsCounter = new AtomicInteger(); @@ -1027,7 +998,7 @@ public Single answer(InvocationOnMock invocation) throws Throwable { Single deferredSingle = Single.defer(singleFactory); - for (int i = 0; i < singles.length; i ++) { + for (int i = 0; i < singles.length; i++) { TestSubscriber testSubscriber = new TestSubscriber(); deferredSingle.subscribe(testSubscriber); @@ -1074,8 +1045,8 @@ public void doOnUnsubscribeShouldInvokeActionAfterSuccess() { Action0 action = mock(Action0.class); Single single = Single - .just("test") - .doOnUnsubscribe(action); + .just("test") + .doOnUnsubscribe(action); verifyZeroInteractions(action); @@ -1093,8 +1064,8 @@ public void doOnUnsubscribeShouldInvokeActionAfterError() { Action0 action = mock(Action0.class); Single single = Single - .error(new RuntimeException("test")) - .doOnUnsubscribe(action); + .error(new RuntimeException("test")) + .doOnUnsubscribe(action); verifyZeroInteractions(action); @@ -1112,13 +1083,13 @@ public void doOnUnsubscribeShouldInvokeActionAfterExplicitUnsubscription() { Action0 action = mock(Action0.class); Single single = Single - .create(new OnSubscribe() { - @Override - public void call(SingleSubscriber singleSubscriber) { - // Broken Single that never ends itself (simulates long computation in one thread). - } - }) - .doOnUnsubscribe(action); + .create(new OnSubscribe() { + @Override + public void call(SingleSubscriber singleSubscriber) { + // Broken Single that never ends itself (simulates long computation in one thread). + } + }) + .doOnUnsubscribe(action); TestSubscriber testSubscriber = new TestSubscriber(); Subscription subscription = single.subscribe(testSubscriber); @@ -1199,7 +1170,7 @@ public void onErrorResumeNextViaSingleShouldResumeWithPassedSingleInCaseOfError( TestSubscriber testSubscriber = new TestSubscriber(); Single - .error(new RuntimeException("test exception")) + . error(new RuntimeException("test exception")) .onErrorResumeNext(Single.just("fallback")) .subscribe(testSubscriber); @@ -1248,4 +1219,138 @@ public void iterableToArrayShouldConvertSet() { assertSame(s1, singlesArray[0]); assertSame(s2, singlesArray[1]); } + + @Test(timeout = 2000) + public void testRetry() { + TestSubscriber testSubscriber = new TestSubscriber(); + final TestSubscriber retryCounter = new TestSubscriber(); + + final int retryCount = 100; + Callable callable = new Callable() { + + @Override + public String call() throws Exception { + int errors = retryCounter.getOnErrorEvents().size(); + if (errors < retryCount) { + Exception exception = new Exception(); + retryCounter.onError(exception); + throw exception; + } + return null; + } + + }; + + Single.fromCallable(callable) + .retry() + .subscribe(testSubscriber); + + testSubscriber.assertCompleted(); + int numberOfErrors = retryCounter.getOnErrorEvents().size(); + assertEquals(retryCount, numberOfErrors); + } + + @Test(timeout = 2000) + public void testRetryWithCount() { + TestSubscriber testSubscriber = new TestSubscriber(); + final TestSubscriber retryCounter = new TestSubscriber(); + + final int retryCount = 100; + Callable callable = new Callable() { + + @Override + public String call() throws Exception { + int errors = retryCounter.getOnErrorEvents().size(); + if (errors < retryCount) { + Exception exception = new Exception(); + retryCounter.onError(exception); + throw exception; + } + + return null; + } + }; + + Single.fromCallable(callable) + .retry(retryCount) + .subscribe(testSubscriber); + + testSubscriber.assertCompleted(); + int numberOfErrors = retryCounter.getOnErrorEvents().size(); + assertEquals(retryCount, numberOfErrors); + } + + @Test + public void testRetryWithPredicate() { + TestSubscriber testSubscriber = new TestSubscriber(); + final TestSubscriber retryCounter = new TestSubscriber(); + + final int retryCount = 100; + Callable callable = new Callable() { + + @Override + public String call() throws Exception { + int errors = retryCounter.getOnErrorEvents().size(); + if (errors < retryCount) { + IOException exception = new IOException(); + retryCounter.onError(exception); + throw exception; + } + return null; + } + }; + + Single.fromCallable(callable) + .retry(new Func2() { + @Override + public Boolean call(Integer integer, Throwable throwable) { + return throwable instanceof IOException; + } + }) + .subscribe(testSubscriber); + + testSubscriber.assertCompleted(); + int numberOfErrors = retryCounter.getOnErrorEvents().size(); + assertEquals(retryCount, numberOfErrors); + } + + @Test + public void testRetryWhen() { + TestSubscriber testSubscriber = new TestSubscriber(); + final TestSubscriber retryCounter = new TestSubscriber(); + + final int retryCount = 100; + + Callable callable = new Callable() { + + @Override + public String call() throws Exception { + int errors = retryCounter.getOnErrorEvents().size(); + if (errors < retryCount) { + IOException exception = new IOException(); + retryCounter.onError(exception); + throw exception; + } + return null; + } + }; + + Single.fromCallable(callable) + .retryWhen(new Func1, Observable>() { + @Override + public Observable call(Observable observable) { + + return observable.flatMap(new Func1>() { + @Override + public Observable call(Throwable throwable) { + return throwable instanceof IOException ? Observable.just(null) : Observable.error(throwable); + } + }); + } + }) + .subscribe(testSubscriber); + + int numberOfErrors = retryCounter.getOnErrorEvents().size(); + assertEquals(retryCount, numberOfErrors); + } } From 1145fc09ba9a12ef855966354636e7ea2e63c0b6 Mon Sep 17 00:00:00 2001 From: Steve Gury Date: Wed, 10 Feb 2016 13:09:14 -0800 Subject: [PATCH 518/857] Update nebula gradle plugin --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 51018 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 10 +++------- gradlew.bat | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 20e8ddced1..3789c7cb5c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ buildscript { repositories { jcenter() } - dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:2.2.3' } + dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:4.0.0' } } description = 'RxJava: Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.' -apply plugin: 'rxjava-project' apply plugin: 'java' +apply plugin: 'nebula.rxjava-project' dependencies { testCompile 'junit:junit-dep:4.10' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c97a8bdb9088d370da7e88784a7a093b971aa23a..5ccda13e9cb94678ba179b32452cf3d60dc36353 100644 GIT binary patch delta 25651 zcmZ6yQ;;SNv@F`TZQHhO+qUhmr)}G|ZQHhc+IIIe=FI-jjkvMT(^?Ugj~T17GApOz zK*Neb5tL*>!C-)ZprC+oq@<)05y;{HXIl2WI|=~-0jVbnsiEUMx;(f51O5L<#Q#B7 z5rO?T=P3X0{9ycVCer~QApg&U(Y??$G!P&lComu&vE)KJjO2kHsAQ%N>SQwzd_Zg@ zG_!n_yquuDF{ovorfP|%HQ&<=A8+^D@!9$v(3F=A zNo)`_EI6nw^$tg4Tr3)gs@B>qKB{hqEeXQ-K-({0xJgWdgG&9d4plxgsg%$Olh@H>R0RS|)iZ)E<8p3B%a)np(4P+H>5|XEh(mRD5t?}fihxrP8f$6{ z7OfH-S2Je1w$#fMo<9?JB*gH14N5_>p7)R)2R3HoLYqUPd`tKQxL!jIpMYWWiEj;V z;F`G{AW;vYXYU1f(?Nx;F>Vz@rwh2#@<R}yvpO8Ql%HswQyS?>+>So`gQ_(N^Ft`% z0HS$LuWmuj_6R3yGwW0S_l@4?=lr}O(CIx@xNOFx5l1v$4BV}=;P@;b=Ub=uz^n*P zv^AgE!mGXD{j~;XgMPSigR@lt3;Z|$CZ=}wr5rAF?@!c)E?j9vxhao*u{3)*{aY_< zFlboep4!tuj-i&FM?BV&;q|c}p5sQ`OPlXXQ_q`4 zHR|5YZDy|ha`T5a8S3D#8`c$^x^<4Nyw3f`nE5J`|P!;vK(j zm^Fr;9;Mnz$N5he)kdGj@aD8q<;7*1@?(a&%IoT^@T$MBRT7fjNtR-p$d&Duhok}f zDsgIr3WAbzcl=_hRvi@)qRTfvzH6}AcL8dx2iiY4@aS-h28Ip{_3W43y0d>yZ>wWD z-Qj~LSmAO4h<|7w*wGT!m5#~)GqGX7@Zx)1FZqV4bv%Yg5?2dbuuQ-|;Xo=850Pl2 z?>B{vP=;uIWPaSwphh|Q*G#z{RPz(@SAql2D%W7ny(wJ6EHSFaIX@UvjLG;CNp|4S zQ7$ReMkz(iF-jWI5Xu%Ye8cE^=;>I$z3D3flBZXj4Jk-D(aDxa&U&bTBl63*2M7N@ zV41EddKtMyv$IY^PAlXT_foL?Q%?AtXhhD~W-8Du!pG?Uwh{@PUPP$BOkYR3G6^Mc zEX81*wGh%%M6M;7cc3arM9DGM@V4cMflT$?T(oZFbQp$mH)WRw(6RzY^>hMWP z!fDdYOOPdGLN+o9EpTKinnC9_|CT0&taX2(DG)QBm7Q?0X@9MI?S?j2G@8geUo8e z?>m6qq(6LAUp_40xmW3*tiaLTR+cqhWeUa171LQh7E-v8X672%xkAoAX}p_4KvZg6J<@=ya-V_M%8p5dFDwOq_r>KLU^&j;kOSno6qCH z5o8(JQ2DV#5R=2f#w$w{(Gg*d_3dCh6vjbRmMV^r6yd`-fm(`{nq@Qm+`K zeU3KLrY@?H@%>FeUQeI|)>lRuydQN~V{ZvYPX`R38NMR*lViHIC#6+3TAga# zcR~j60>7&R&3zoCAjb8l_tO>9hV&^Af5M*c;N=dZ%iI=E1M3tsm_E;ap2rBKds=Gg zhH63^kV@?aPX}z^GSHEQBb=+%{_7pOLD z{M%j&TU%RKHEbQPZhCv)UiY>?Y2E-99-P@`NJotQ-xjC7IcI-!?)(f5zCV{GBdv%5 zNCg6O>0^|!B!F1MtRN6JVC%8Ta-0Gdg8F+f#E0y8aH;`RbGszAWKg{c)sjq{1Cl(9 z0^Sa-tP2=OP_|UdiLXqU`NyYgpm}5Y!(~u6oO$?rY(7OFnG$Z*!w=xB;bTy}DRbbe zW0#Mbgff+^Drdb}b{NL{Cy`Lh$(T{#ta;fIKxKaVB*3*Z!Z$4@7r(%y?&S%_yN>R_ z)k{5a{^SY7t6J5|Juod8bL_IzI-yLNssV>z#iHB?2;O|7p$ZDJ4xua*-{%&?b!{m&0#e99&&-k;r3Aa`-jR$ zhg=p-DBuOC)wz495Xk<_Looc??!8m=_YK_v1BZC^^mmVH9x^gTl@py1egj$7JJ;0MGe5~yy z2d86KOJSw^@>tnwHYaNqL*cPqEXf&o}$~D8^yEJNX3Qp`R=YnQQ0QJhzx*h4>fw^g=yKYJyIV2 z!ufEiR#)GBvIN;a9_pmi0hZ(U++fji%$^ciDw`*^?M#F+CJgrEZyx@#?WWZZWh=fz z5J19HEt_dsYt%*3QvBl8F*|qXhPiG=&&xx@AY0>R3^skGRd?b>iK!AF65>T7z0Xj} zom=_(B}=j#t3G$MBMGt;Arv{>E(^{~$*5afHH2<1CEk{?M;S_&`Isr~6kd7u15RUP z2T2O=oSMf}Nr5ufHL8WLOdlsDi5x24BY=6hYO|*iS~be0yv5@pI*q|rUObaRQtTxx zM>w=z)bnY=fufZf1N1Hep?HW%R>9`kyEsA!NgT|Y2~`_AT&`fN$&R)A1h)*_F2+GS zP3@ia9tA?Q5~?Pb^zY8nq1x0=(B!iDGfQKnAu8Usaq@gLiHHUFaW82N>!w!CwgP+g^|NOnqW(H7s5&1o{i zlweszZN<)O+LbDURjkjHR2D5=MhRcth*{7y5``NhwjxIXJ`quqxY7nS4KTwXJ@;%d z4&lV{*^o|@ro~g#)+XAjSsy`F-fg;u-h)!XId^L(_Nsq3b#4M5@`2`gC+i7sVQmM*)#0ew80CcF$Xfd*a3YuaVM9~Pk! zMDJb-IdauArCvw=keHEqHupE)#+J2BNX9W2OG3j-oCQYV!X~+L{S(?cyM)VOW~vlY zN3Gq=74Bk6htA$4t=Gwt{2nFgswnDaLbbP>o*bM}C9Ch6+^vSN1~8(JkEKu)qr+UV zkDk+QFalO>zCuXbydBGldi24RYF5e;7p=j7p?#ZW8J=?goL=L?7Ksj7m}*3ws{O^% zf{~xIChaO4wuVxu&W@q89frY}I*OS$bkumd+Osz*s+R_xj&ADV>Cj>(MNin=*3_}8 zaE5TS`rKoaS+?}_3<#3xFZ5LBk^83k&Iu@7BH}Mza(~87tt+%u_X#0TzmVu7h#6ON znkt&FYRPs`y72i^Gt{l#_sdc3gJQ4v{_OX$MXKYWuKEw(e$>s!h|DZ#jr6&{@CoB% zdVzRd64d!xtGjxID%wxel)KD3lD=JI;WVA> z%=WM&amB=0R#e#L!a{D@)ITF_AOJTVPotK-jMH#7$`)Vhch+LGKhIJ*{mk4|wL~WK z2*CgI*T(5~{HZvqHe3JQB@d*j4en1J9 zJ}y6U0HE?0vx9yMbz{py77m0~=?$C|nU#owdYy6=@`&`x+c#s{IHErbKQjhvKO&m{)s@ zsRbk&qEs5Bou(RNOkw!Y^iTOJsYVErr5Q}i0dNf(D!)K8IX;hVIm#aYc+sG=iSm1+ zfH&$oMqv4p68`2^pSm$m>@aZ zZgmZL5@TuqcJdB)(v(tJDlh+-1BlWDqr!8Z!czdKSZ_}723?R+oZvN=r*#q3v?g1R zxm{@8iU};niX1{U-VBRs^cI@11$u8z144K6J07teT2(dXddZ$HE{^4LuQo>{I9`X8 zV6X)(R#!y1A{UI;@#aopz%Bh|^daVEl=jycF@kKY|e;%le#jz#OeJ^^)WW@-nY|hn+S2ZWjEdm93L$_x!jT-E{Ol0C-T0 zjm>yWmMLrTdHKC2m3)nL4$I#fe5B(chv)(+EXlEp4zQA9hT*wsH?A{Om)nxvtj-y4MgDZXn(-wy!9|uIW}6YxfW9vV z)04cx7=e~x^UNtYUxaaYozRkK0%RrXt$jBhxcaVFG>J4>L99Mci|tl&r@2@+6R0eHl>Ckb$uN@tyMgAF^HN9jQ#sp$K-=<^w~NyJdGumZ6%SUl@o z%qQYna9E&cEOuTzihJ_30~!a3Ipex&Dkob98=@;v56!D^=_>}uav|Asj*a!;))P10UFca0p?QBdOpDplOgGLw zzi0NESCfS(ui&YGEPYt|vljRSjcTtnz(@3#@c!bb)u0&0VMR^i&b4}r-RlCS_v=vD! zFwXYD&<$HSL)k|QXjoUs90~p@)ZzzPS*+QD`HC=w`x3g<=Yl7_xE-p8(BO2#9|Nq) zK!$?SXZWsoz*mvmS`FvIHxmj8L^VMHB=M-&miT@ND3dKQ`2}&!eUL;*pE%-hP4&Rb zw%5~PXB5Jc=RuhssGvxE8SoRF1egu|NrP>G?5e68#W3eGq;-IzOS>+B{hXbwI)xKO0K5Tt~i_3(DggHWs z54I@(pOIIdLeC-m#n!CX-4eV|6(~6m(m0n#k-J5^UUxOV$6o3|pRFR0-MKm*nnJQu2!3B(A#b=My3Mx#E^CD|Cj z;}kF>u?zS9%-wSm?W7HUwG{9ioaM#Hc*S3SCjIQU&T;d0@0(}fv_#zx$^d@;7b$|r zlF%CwUd35g{ICj`#}mIDZpque&$6;^R<#4CjS-)*~r&l7JenrNi%XQ;V???lPaelUF z17Ni8gR}z83wE#0G>;|3^YHs7v()tq$#Y)2{_l;j*KqB508~ZPS@{C)vnuWuK>XZ$ zILsqY&!Uub&i)-`BVWVo0=etjdFivVx3h?8Me(o)l4{>~!IcTuCzK<;cwnA63!+6H z(nc)U8X=U;F_mIL*F)=MGSoY3J~%<8p|fWwF5HGM#ywBHnWhms$G~B@+GUO8z&5a_ z>6el2Bi_zUqSoL_sHh0n!(S3Oz%zU_Fum-F}XdMRgZM#JQBbGAD*4-ZXZg{c3Oso=IJ~Nd*@>@a2or0^R@m;Xs>U^|M zRriY%YeHKuTLDC%`^(Aj2CS2@I(2>##_cEjXFN9U?EvfIi}BlVYX;W{Uz$B21N)`n zEP-`yloE;xXym-ee1n@_aFED z{OyV;2xHWX@_0-iu!B6H8%#Br8?b{ehK&G#C_He`?i`PA2Y0F`bVNMD_p(Gn?_i9O z!FxW|7gjqq0_WbRANDolA{+KSdHyu>>TmEP3PhZG6K#4l&h#e>#QxZ09F_!q&+sEm zxEW@dhG3%Ihcx%13JgEtr5qmr_96?E9E}AaiBpWqji0_&W%+ai%10)Qdw&3g z+~XzYeww{%3kqP4Y;s`@BtL%p0U1IaL<9R#2Qot5rvmlSOe=mov=ViN++00=hrubT zA7#I%Nqw{t?n4)0?xU0M!ybLU|H^nBCJG24gnU5#VhhZtuGn9~#mBzA!NI@4$J^lN z*=hH+@^Y%M^fHqZuDD~duU|`CQ9%XZD^JUnP;gLdE!meWrKL4_SWU*%8JXt=*3DVP zON#uQDxFPm%%4W}%$~Q+?c&by*?PLQ7!E$__lkBGlLim-hobQFf*%Dfmoe97YltY) zYMo}}HKI($74GoN*6SQ>Wl1gl(1f!ZyFt+-+%C_c`Qy)MEo0MCPDvEH`se@@M_Zk< z8~H3sUdbrXYOYvkBIBS{qbXnF{L3ULnoY}}ZrZ$A$}GrZ&=6OzndNZYG6|a7$mHU} zyoClwXFoL1G`;!w`9EK@!iFk_*6l|wwA>9zN)gS<#@t=S;$-<^GtzbnGocH2#edDV zh7lLK*9{bcpw2&+WfadnQV0eNRw$mQVIl?%ruNXOEi8SBsI1j*C!luAW;614k)tKA z(9)A+(siwyV`1DZmwmE7NU?&GJ1>-HqKcK2MIhiLI*HL(apj4Cw;qfCCM=eS&Kinw z+GxX8eeO)U?@To6S?2%{+V6CkyjD5nb2Al0rnpAs=+=~U^hr*WoqgVPMklpYS5yR6 z8YUXAM>fNFQd4bTMbwyLW0$SwOO;QyAJ{S3tjDPSZ3TZ>XLv-f`T3hFgV{q}SI`-_ zPrR+yEZ~*3<-Z*$Z%TD za#Q7UNRY$U(AuA0vbLP1o4~w!zfqr|~#8rx|OI9WC{0j7mQ! zr9n85L8r!|`3V$M(cV$5*Voa}e4ykjK63An9?cimCm2t#kgs}=cKTB1xa!GXl#Wfe zVq+~ma*T_~R_6oI+FhnbUQiVm^5Xn3bpM^$b^`mW%JD;v%P#K2mUzv3E#mI80rJ@Dd)p7#ES5c$iHul&e+4sF1wJ?gCWwgl_z&xfD0Zc9{50|VpCS)CjS zcKXt*<;-s;T5WZ-(vZwuWuOezUoyD-j|_JUvn@A^EO#~lPZd-2Yv~9hL*7E=R24o} zM2P@L&HH!f<)}N*=vZ?6xHE>>hP)yg7xqpG&epJOm{@u}S9*IG+e)vWxu910o5MtD z{@U<2@}$Z84g!uBYEwq^GI-Ke0s@&!IA5-05BX$=Dcf3qc-3d?(KF2{uwEDRc9@^( z-j~Zx6D$S5Xgv*Z=}&4mT`CI0-i04UuVF8 za(3RDsfj}l1lnwl_qA3qrB5=EPoUE6TZys!Ed<_kO`gxS=ZatSiMZ+(ooT23wRQGR zs^%jCEqBgzuh;y`o{B;|)n4CK)Gn5qVJ<^YMLGxIOi(_gy6k`_{m5L@j-jfGLO-Tc zDJ11%v1jfw3S>`@_fX?chOThlR%(XF3iz_Y(57=pH!lJ*_YXS5Q#_ETr$ z2Y4x9MA>m@%VgdZyxhCgL(#?}1+y(L>cZRNw`vo;NU7d_N!j_89Hwvla|VjK9q^ zLoG!OdM%mYVS!dE-sc=*iI?wrvc%KpTvZy-KOtH#F6H1cC+gCCKfOT#_Wnqw7Wb&H$MQQa`TO31V_ zRyQJ{8k@G1+_sdww#FtNG8omlqpG>164}nN&24Mi zwoU6R9V7}mmo#>MexO7zlVG$Hnj+ADO ziYWeMIGRRF5J&1WKWvwjiZO46R~%?9TS8?!Uyv7%Q@+3~x~8_)VO2Tr-s6thc7(RhG^l7wjEbcsVi5mc6R`V z<;>9xwvM}oF#^2uiAULaM6HUuSO-mS6brZbJj>?EZ+Mw~yg3VyTvX^>S$J(bIQt_c zXnT*!C%elpzDtnwt^`qxTr|LQY|A^eJ;9Am;7?f~ASMtn)WuB0|a?NZ8y zp5j76u^=vuteMkA!(_T=Gu0cmlBHX@L2DD8!|r+YGp?X#j}x%$17=>!T74@f9`ey^ zFQL8Ap4BAO9h9OauT9*ONw6~%{lGynbFKjvW7Sed$(T4uW_4O0<=*ynAq|VO4y;|0DRnRdEPTNp;gdEi3}| zPYl!iCx#XIumO->XyfQVkP2JNHtJl^CBrG8uqz33fw7GPNZKew2}lJNzwCQ*FfQ&d zGSvq_480wYoWzfM<^ZiU2oDNo%59CFAOvD z3!vP#qAb2oVEjtrV9jfJ?qDw%R|ylMitp(yu5WFwF6JLD9}?f(-``wJ|HY6EK1)io zgA*%RYYzf@bqNXPXMf66n(8ioJhr#8flRtsP8f-;BF$Pf4)HGxxz#V2$BCr?U-QU} z1SH-|i4ewNA0|Wl`|U`Dcy4VATdvni9805_u%VKiB zmeyF&@qwm$@bvttyk>BX@xU0HNkp!^7B^5nZ$gyTJQd+lz)nmtN9iAf1vqh}x%rM) zMljnegbMKGT!H>db#E?a_N4@^<}tyG)%;zsem3TkSS{Zc^Vr45CTuW`Z?w}e1o(#J z@l_nB|5lSQK6*Em)BVVm#>Q0dT0&B1&SJ&!X-ChwqR%D&II}OnSk&nva??8NbUU5Y z1G`NJZqos=Zi6*QkK-^Vj}v!D0njLh)hC1RA6a%&uj9pL|4HR7k*7A{h-IN>`-17$ zxW|R>AH9ok)hjsgqOn=9#QhsjTXFdI9vmikqzLmvFIXnMpfqJkhAVo z*_Km9X5C820`h1PBJxx*75A#_$|2iY5_QHMsO%u4n`9#Bqe%*;of(R;nqrX@GUAye zQ|ygWU}(yPkur^1$g*t84Is$AlYV3|7V0amR=! zoM$=C*sQKUA8dOf%YfB4m-z=H1PuOVcel8vm17o0g4N{O$i$TlU z6=;JRc5ideeXPw#nCnW@zBR8naBVY>MN?WF=9=p|E&kocM@aC|TvM>REXm@DqoJve zQUOcG@QRdGnredr2F&!ay1siVVRLUxNLSu&)mjf<1*Eo%TEIv=;!|SDs*$T>-mt1y`X4ygwArtB$8o8KQYIYytvCKuuCNa;GNlNnEN zqCK58>0vW%-2F7=t7_hJ3`SBxKeLjPm6Mmg-FEu93N9HMfrSn+f zt?jJ_*AE4@3x~3?Av@af4h<%ZLTTd& z_Ro?ML~vh_C4SALOEs zM1*O@T2kC^#}FeCt%T#uVO+fd{JjoIvc%kavQ{84=B>_D_@Vhr91n|WB<-v#S6~V( zB;|J8AiH0u)pPJ}Aw3xP?P+~EA~#B+1po(-m*j%rYCR=W468BXQZ+a$NpPO?B>XmI zSv-x7M45pf9t+&kDaWx>}iKA1vXbsHzDGjbz_b*!* zHpn4sJ(gcDo!4E>8|vm84==Y(-RX77K6iBkVQd^_&ek~TrH$6}{!|xPL#Ksf&;UK> zI2Iic-K7m5#42(or_wuQJ6xTO1>VRTRIfE|&CE4uJ@>UVb%lMsTbrgJmOz6c2&fxT z@;{k8!E!~_-T*|49?i*w-KevKz8vW%J-&FSen0x|E`eDjjC;?(E{LrqtNrDBFc~|4 z9z1dnCU;K{c9mrP*k5yO9-j5tV}K1(=Czv>WzFSH4Dax+pgvdM>*n5`bN;glfAY44 z!KF$2SH_0i$^F^8-Pd~4d5Ax#Uo=IhN>Geh{?M(Kbtj2Fl z+NH7ZDGO4vj^#H^GOb78Uc`sA(EiBDT1d?q1YV8!#bmcmH+z=l4Bs0_RQfWe`@0ox zNvYZ2)_4g5`C@l+zjLo!sE9*L<^E>EX&D>hPVNrnGQ7<)ye&~bCt5?3%M}Q6#yiX$ zsa(1s_t>CZmHxl}@8%q`R{;bNPzyqGfdn-`)7BGr6@7OdM>~p78V?v)IUJ0Mh;~B; z4w_6s1eG^Xn3-7NXiQ#rY>6nIHBVWRl(G=9pl6750i-m;lMu;D7FVZ+s{}sYqqc4} z;94>O98h3R7;84&bVTu@Bk;DzdY0FG$NeerJpb3vg!RbI>y^+g`iL(2?+ zLZW3};$9DolGMwf(0{6d*PhdMqgVrQs4&feu3<(ILrrSmX^s6x-4ZE3NAm|Xt=vi5 z{f>4D$u*KVkNO=v|KK z#xH!Bc*=AZy^Iix;w>FUALVFePkgn7Kf8F%eJrNjr%Ki?GtaNMrYhlH5>qY<_jK5r zsaZ8Dz%YLbj+uh&(V{8X*XbHF=3@2QB`oRGe4DBp`I?lgJ?SS+6& zJ`0?kFzcvK(!;zaKEFO(#@So}qrh(!rri<>!m%iscNi?EDb~r1VrvFx6vII<81wG_ zp{uZ(UOh^>CtYFG+ZZBy#8e)H(^=q?w#-48P;G5>G68qE*-XtS)|(Bfo^buY7q8|< zA>5t+g#?pNa^s3`tg0X-+8f60zs+Zj9(>sQ`Ax=9ZatdXbj^(4GJZM$)2;{QJON>t z?skJ3__z(qlJya~PStVp8#TuqJ9Wi~jEkE7RQQah?L(=tA)D$=*Ets#@7xB953oH` zJ}(`TZgSHdi1im<_>SjZsw28ZzPNW(A-*tSXIJl~6@XAfR-IZCw=M;~Z;BvK_nvT7 zp1pBMOCiE(KR-tz6xO|odVMRvs*EIZ&4LWEJZKUg& z)?%w{U+4P$cqSHUHhVOmCFgy~tllH^mf1f;&1M!lbLN8xmt5-UdOe7(SB1qvz;JRb z=vuw~OJ!45Zvh*iYT7M#i9UPj>_p&FFSO@zkpO9n+jIxQYxENldwSYsn+&hH7!(6j zM(%pW8jjS_NG^QB0h2E z7V+1AV{CJKb2}PCY-2A&M-Vi%%Nv5e@)kQp1==1$hXy%Mm@CyPSJ?Ivj)ZWr37nTo zmzyxg53&?saIqiZSVZ{9btErB>*M%d4dxpfH@&}9FoQ^kqqmb1KR>B8+`k0n# zO3OPe9^FRNye4Zb>&1MeyfF)geClA>yt(7cjfwRKhh&S|s_+JXIYVpdFS_ebc-OQR zKZ$^)v-v7h0J@-RX=-ADI<_X{em`;7ZSy%)A+Hml&7vgC0%INSVOb}%R!4*v)}U&6 zgQ3?JT|m^$wu5-gJth&N_hCPB;#V?93S&t18CKifIoF+}9p-!*Lw^TO$5T;_SH9Z6zjEI>Jq~G~tGOW5Aome%ozuJUgKa?GDvl>Ak~fMC1`~X8 z`B2NQkLwFiuXm-(!nj=dZ%r6u7$-BknW5x6R8EvtAKje7vbh+#-Y zXX+jChU`8FSXh4D`2lar#mcNZeaQiDkb1v)43yFH!c@(7{CuEE!mEK!ca#V!HV8+g zO207>)cazOw&j4{T@!h6Qv2)wpXxyKy&MF{e^r(wQyE-rC?FtNL?9sI|F7kT00`BL z1fW@B2Us+Bv+}}mW1whJC6vM2-IBm0wXBdOcxTJiTG+Fo3=!nOvCLhZ%1j_&X6b-g~)dz1s;Ze*7@@kHy?L?jAjb+XkbSiW`W&?eRk=R;m>iTORhT@rdwlSH^?QEsea(AN0J!4hdtub_ zdtywvIUniqI0}()bY_`xyoLKXGafYl18STt-F16yj&V2k@ZtG;dnW3Te~k>dzA#a6n_ zAx3L5Mp97mgsbrvpxV1~ zh$H&Ere&SALwi}So zn>CmTsVmK_UAf3lNeg##mKo#nFL-yY7_&8{CKxkF-x)=Gh94U3s34&qRRjv7V zhITCh1=pmXhG*=>2^D!wtjB6`qDDaOrxQgD0`*hSU&p~ zxZ}io^*?t;9v(1$h4VvVoc=9^Ubo2E87X$+)r^fKyH9*hRqG##lc0q?_7RWfcWy z!m|{nd$b4*ty}C)W7=!GqTIfxL2?hHuKU4L|6WrOcX-Qw`s|m6>29h%SiO0+xlr{n z?n^$jYJW>Tm!0nDT{;1dc`IvfH%?m{o(7*HQxXzr+t{W?tzn}l`@E%t8@X1mow^|N zr)N?QP^3+*gThhcq^!EIby?q#!OW>T_U(=F2*a>_d7$zacWnYpU0o9Ts)|KTmt%be zxfx^I03T5dG2U&|hRW5bucL)0vmG2{`e{o>H)ZMTG6uC;Z|?H4H-B*rx>!c;FE?|s z2EPQ1(a+gg9vqTtGJ|8K20`xnO@~xXCYy~EfdBdigV0@~xs6~4&T?_C<9GH<%ZLd{ zAVi-7m7)dxctx+aK|+klToa}nfw5;UDIS5LHrC}X>(0d6u}9zTUL+8Q5hnyU!BO`@ zxGr~ptxX@K@q_;%7o7mJf#Br}@a<-_zuh>OYv=lWfd3mx$!%Wqz5839xTxp8_B%BU zAm^cGchCbg$d1$b%_5`$Jl1L)N1_D*zayD;SDZKEkm0VjYwwBCn-uxiJZ>Zw4%g{; z9oit)naTQ=(>>nrX{#O}a$fisgAuzVz}mBNDUG|CWaq7kqygt){y!W1%~gPjC(0pkI)!GUR^ zsrI9Yr6R!qLDlLe0J-m89(>1d0Trivr7vP>0f(mGU%4UgRihoFfgYs;jvR%GXv-6O z*B{(1cT7H5=i6PoMzTfL(rX1s;V~5GNB{Yc2?-$)?g&6mfQ2>C(-_^ugw!Ju^^QWO zyOU*N5vkJ&H*tSBWG@cPyD%9Q@RkUV(D_cZ`3|M}L1oM(ZNVq*z$eO)_M_K?Xf_3i>gnw!`NlyCFJJvo@Q}kZl}nmR85?B|2Bg9ONuBDN+ReX1TXOzx|B1| z3gNYp*#7A3KJIL)^6nmQZsDOQ^cP-oYiF>YyUO z`|{qXoiWI1Z!~qCV;HF*Z*9y0)nAYncCiYHj6DI2yqa_aLW~|RIT3o9==g;@pe*!1 zTfzgYe?k6t5~NEMK#=UAA&KoIE3d-Tpv2VB!ot+R!lC5k0k6!&!o+r*yrw}75Bjey zp0vfmJpZpjih(LSI??)|GBtHChW&C7mz)4%NAtt!5Qk>cc+DaUK$o=@u#fnX?Hx;? zK+lLl^Y#_Q*&B!i%`H@3hEe|RGvTe^xF0fv*QExgToC8dN|_hS_#AG2e`*BVgeVZB-kUbk6aO`Ck1B~3?_2x`IOJ(hoi zkeXVa{pwOAEqx)7wCuEe-GYUre}z(*T?Nxg&=FOz~2=r`Lu#yaqdc-uZ=$C zKs&z$JsBZy&1oSoTaX}EI%N;I65Zdh52Z4Lt-qzDw^1;dcd}wcrhj^&yFP>PAf?$1 zkl9wUO7QzpqzAm3IR)H9$;nC(?jBF$)~^!<-KQLn0r3f-f?&6!SN^n3LU=%hIA1Z`1V>)#rcLxpUOd62D-~K^`T!59o*mn!NO7!|_^w13(3S^CxP_I%y8j}=f6sVk zN-@*U&~>WD0i4;FoFDo#N>Ho%bZogwhaz%>vycd5vP37r9}@mrZo%j|Pl(^Mw!UaH z0;JFw9Y!<1V>-`6+sOjc02F&$6&_U8hV*2+rx9T|0_rtYA;#bLHL^!9ii}k^+Ix}# zewd%LSrb4y?MMn{nie4P+K@?^Rr}0pOcRAtu20pswqGpayZ2kx;ly%H5$ zesS>0{ysRkDh%G}J;+3+XSGI~<>JiZ^-J7nkox#kPk2XGK3aw!gwlai zp;P?_(AEb*o2-IZ)CBqBBF%_xjR2i7fykM(Ls%9EhrtG)-!)J3zUYYT_3@gYj3!PY zOcvIUTjfSlQIPV*m@_n1I3ftWjZv6w>a68&ZZ-rH=2p=aub-R9l~wrbEe9#WQXN)<>17E8iNkf(T!|714r(1?FG4<>q6L@T>Qyb z-wHD|fAD+?N-j~3UrbT3?qT11A<@{jg?25|^Q^B9ntG%`&=i!5e?w9DZJn2cfjWu6 zG14kUC8@K)xw>2pn2@tS0OC8a)3Uqz`dJq#xtN?INoI}RIha=3H+g3`r3O)Ie>lt% zj6sTFu0a>hKYndfL(=5RFyi+eVO&Qom;-;LM%jzK@9JAd%wU3w7NtkHpT%(@ov1ai zrXY078NQ1O*#ave3%!YP>pPJHBjZ=KweM@LN>5i@(%B7|Rct_%ScjS)7{JnUp9#sa zQ2g`)4yj9KwtinxO$YH7-4&>5FGay&)B~N~+}`Yl`%O`e2u#kT>GitEOXB|TqTlTz zt~+ull?S|AMU1*Ne@XjdbGl5iLp>?$bJU@xMQVMx#?LAYIYcX(=0JB^bh*Zi6a!iL zh7 zYxYq6vT{ zCYMr6z(lMPFUx6L5hdy2tKqK%Zg_IW;4lh zdXM1E(57*oRt;Uir)GCa?BaO%3eiYd*8fiIOCs3iyh4)HIyHO2kJ^zeN7gGi57&Zc z`SqN5g*NFL@ZE)dm-emlBEs2o;f;--iY*9mp73+3)6@&qm}aT{r8^Wnd#65NHHG942m>T86V|v*#$5o#c4$zGPZ1_ z(W&0j=Qd8z^(%c?f+YzAnO+HYz=9Na6a2o_vXN}G{$jkTCpSmgzV!f#&Wj}G+_9)s zR+dU7Kt`TgtZO2L1Y!~}`tBjuCnR4LWTQy!twG2u{WYS2{A=w{+|55J0u=fkBfb1$F6I=kG2jU&T21avHo2)iqIp&8z;AkTOe!?_Z{qLgy zlHP}$(rVH0?8t#ZF7irIKK(D^;sGjo857K>Duo$uJT>!+HDsQDR4FPxcxPGYxgctB zuy?wsJLhigKIdkxyWM&gAOH0QCjHsk+OCLuY4glr$mjf7;J#-y69OPK=?#wp6YU3A5UkK`@8mb@j zijb;krL=Ojl@mp3r~SHW+L2x|p?!7Fv4hUQF^@r-ZMf2)OS)2Zll(_Q^Cs!s<8D-t zW5Y{h!sB}2^iPL!cD){Z4?PAA0cGQuYtvkLf!Cr$TOQPQ8k~ghIpUA0$=}Fqo0{ZOxAJDJmI}`snpuE zORha65KL^E?ERL3ax|)nS8(x>+}f z^sAD^yfHVbbAyxyv7&@MuZ4%*6&=Xz^&s`*wl&C!v8suibLr3}tgv zVkn*Dy64nX;^A1C5a2F-G^9G&lCSCxp@;NQ5L5%Kiw6k$BX78?HNc4PGxE(FCo#T% zHw$c_ns^_urgGUlZxzZJ>6!qHQI9OCgHOv0>=RFtBLB(XXK3qXN6h_%D>~3F1-r;+ zYVz&AaYBNnMDJm8Om+JBXI6*Vz;TYXI=B6iUdWo@d5t_aPE8)IrKdhEenrBU-ek#H zm?B|*`mX+M`I-+qyzF3{c~wYxG)sk18vzH4dY?)r)T*>ok}sIfu_- z0z(H3--D8!X(65S6S+fMYRo^4?JEgF6%&Q*>Bmo^(k0UH5yXe~$aF|h;Dg5`)r!+09o zh$Z5JgfnNj@-Kr)f`i?V&YYN;x1l5aRwGz-gK3>jc$980Mb81)NW3oTB<8*JwyQ#0 zjA+hgO>@guEn{gx+Z;Ko=@EVsyOBM>*G7jFi!A73Pf`ox3xb5t$aZwJJ;Qsh(OY6l z$6Z?6Q#W=nev>VM0ZD@qtbW$(a3B3bQ2oS)`Jkj~Ox(>s3AOpwi1YHkyIih*+dy3E zKtgZ%()#Am@c-ZOD zx6C#!f+*JU>4Qhtaw&4H1rp`}uOH=FDLotwUMrnfOY+KcwaTEcU%W9!r8jAvQm|ty zeQGa;mv3vzyb0_?=Uj1&n#~X?{qZg+sa^S(Dv54>xa>D80Zwi&pbris?^f&yi{GaZ znA8;$4ACBCu-W|k5#{>nsr!g;(@wpx8?=!#|GW^3Z9@f-EY`HNHo_&>0@?{stJ^cv$Mz3(ZGts&7`W$A*_jm3K(7x zoF>)4Ffg4S<`8+5Y2T(Ff>Z_n0;t1gb>TAE$LuV(RSp9wMEO{j!`a0~aI61a-DT3P zO*{StcWm~`vgeOXxy$Ws@>guPP1_OO#Ss!%C}*}!Hfcq}f_duNVs0a>V84bRCh3F& zBBdmFyHTj7Ys(E~4mR;(G;j@V;siI#+$C8j4&dy}KimO@TijEZJ)h$DIvM=~TNE%o ziBht&2r#E$Pu19MjgYF0E!g&$U-P@**Uek?0E-rEPt*JwM#(kAWMiWR36?I&jn6ge zo8{K}=L}VXrL-F*78#rz;?uPVCC2orv^e@c{Ap6;P8$8Tw>m$sH9e(8lM$+*!5_2w zgciItUQ^co7VXd1y}6@gV?)U}V(sSXuVg*;;!~ADgy^+ejT`!H;jG2jl%+O{2_;kt zbS&eX4AUMuGp@m0EKcR~3C%eTC$RctuBkaX5MMEh=L-uUGIh!s<8GBx^t{qJ0@XfT z?&H8B8{H=T04DU$L@0dH9nX<|unes8#)N{sN_g|X4mpL0p^>cDC7=@RB&)slE#rraeVry3WTOo_O;OId1G- zx35Yeog)?={8oqmRG5u=?Xj}FTTp8VctpF(FX3Y!Xf4}CmQBv;pzB0tvX%UgZKUR{ zUjb{hyQs=<{(6?xb7<6XHsT=m(43=ww9`~u%rzk*sy4RD;Fm}-@qB1$M2sm@c$WRP z=m~ma(ta+|NBlx0ydqwR+^Evioov0b@kBSK1LFD-o`ivgX6{wuBfy%ZJiSgJ_yD%7 zFcZZg1(&_YI0~Dpxp9m7<99i%@mLNM^W@AtyTO4^<9!ssQ?-y{&Fxy|aCNH?^=BfI zpWgjOzN*+OUH;Q~LR{~kze9C03`goLw!wF*Dxo@4x&AX#GRHtLGgfx`TvbI?+uPmj zg_J90c=u%Qp7Z%Ae~y&LD>ttkYw*YgP&r^}T*`!EmEk6(14V@U2z%pvJd0jiuzm%3 zDRgC(LU0E=9J>*b>6?p< z7Rh4P_6)SLJT+E9&}f4Q%B!+d_a>%>e!2Wzb^@OUvWXz3(p1<(weC}Vs(B2Plb1a9;-^Hn}-m@(nd&-58d;l`k$NT(0s!v{O*0zr8r z2Ouk=f)N;!l)2^nA=Uu`h`N~caXHIPl;^j+Q7-MDC^2p!8}os;^&aaJgL0-!4*kzH z`8c52uUOf!*xLg}$G`{=VQ^(e&<`Zq6g}pYh*yB6aPA1+QME1?R(&O+Q9>P%=I3DW zoEeLwDwn<@s{@|4e7T0SHmL|`?*tcNyRjGbh%lJN(tkR2#Te#)@1nMrq@<3Z)2A;yD9{tcpl#WHE17v-fWlHx3^`dkz|s z(l7PYITiq9i{%DV1u_M=XRHW%NXgpk=6SEBcZ)SEaQy zrLRg?&#CV*{lc%OlvE&eHJ)~X>z^;^Z+9a4F>-ZF(RIKE>sbZVpl zW%r@bv0I6CYnmWhm?9Npp!eX#f6ByyW6*Ha~5-JzjG`*?3SN7-Mnxx$6Lhs}#wR z;#G|$&NTBOT*5D~?rl|7epBoag2xoBMl25u!j9W^Q(y>w%h{-1Tt@0p(Dt)DTTMpu z2|y?A89!6AzeE#nB3i&V)G`(Mk72Eh{p;`8wS&J-Sv;%%Jr5u2I0h20dPenX&&xaK z!c&QOT<-LZlRrJy3uR(q&z~x-rDZGp#lHA?Wjfzm6kJ2j!EeVg4WI6KIs}8Z zOlYXq(@dj9LY)jy5VIF(x9O_JLxg>mCwt8-tC3cyLC?sWvbd@ZleN-6O%-k^T*Uiy z49>H?RE5BrPBeszRN;{7V9W2D;MKf>ADPp0sc&XdMs$S;wT~LO{G|6^iOE%^cA ziPICI?8>3Wf+ZVgr7YX1wEPdLuIp zh@&&Kr@rQJLu=xZZ%uBIvVJ1GlxqfPWZ<|K@;^JD0EbMDoHBMSfOrh0qLC^v5WIlO zugq-GN4_K0{Sj>rE$3Oz>!HnqnE5P1IUSHM^}OP}q{U=$FD|vOP`GVIEn1Lf7iFz> zcM-1g6}X-!2$))mfMdpvHv#IpQ*7+U$1YF0*F>_ipRo-FUv0*krX%T3C~LOG8#lW@ zzcgQG1?yE-6n(16A)@CNQHh&yp{K~12Mpc(IE4||)ydj~t++u>sYYzeel@0X;zLhk zPn$(JQVK)y2Q$r;Qv5iT5#bNompN-w0nfCia`;*JmsYndx1?u34a5?Hi29?l@>-_5cGS)~yO3&KFB1l4wKWcq0>Ugel^V2K&r{kkiE1f6_^1R=? z_yE^EUOMx%*(Pj`Q%Lw1S!?H6A!^KxUkvueabXtG(8<^)xnY;YMoq7{sBWbal)*lh zqTr0dEo(-a-%9T6&a7c}Xj~#g{2bq9hS&0*OJ18s6U$9JIan+{8WOMRV)c0ICv zo%k^QahOXM_%U*`({80vXO(HwZV4|m?PwU5f?ix6KsSd6KzW=9oy1bqO*C_pLn*E( z-+t`=18Q~F+8uZ{mQ3`gUBzzbkoFJd?I`#y2bL7)?rH8sah}dM>~JwZr<0fXTKl4s zR?}Q+8pJ|MM<$FXRRz?DA^soM`-D$^7twltEFugelhLm$4Sq+Iu@Ek#%Qwq1`b^55 zU{=KR+_%?c#Gv@5&hM<=v4L>UkvP@gNo;mFqnYAZ##Aim46%x@!F~%cJM8zHzKW07 z&x(BHgxq>x?7vvZ@84Wu#T`Dx#NSFcNK%Q8s*(Cv zq>^3rpGc)%4K@57l{&I;6dZ=YcEzss>1$2o2&{+nGRlv0-5G;U1{i?}@RW z)&d1v|6Vk)duwQ6^gkHrRlpsO@ZA9=ZT)*-18yaL-sNJk{9C7~uc2`!6g3GwpZq@@ z`dQ^e4r#CDr1~G;_D2{PSV%bs0px+ko#!6BxvOPg00qrKPnt(yXZU+?Av_jjs0sZZ z{13x70KobP{1)LJ{J{B5bq^+6)t~Ezj^!29BV3Qb$jJ9#VPvZR>EA!h;QzKj0r^}n z{NVJFdLZl-_Pvf)`Q70AZ-oke_I7rV8?Y|U;7jYA#HqU)gz*iM@oG7ptTCP>|DRTd_K^#9fRPa^uihb>y^9*o*Vdf$f+R8UrH7yv*64glbPq}4(F zZ)edVREoq9@ZT|hln?kwz)SO9b6XSeUh`e7|0NWl1U(;5#i`d2Zw)#KWivX}-JSNI zNI$e0(7yfWsPZv+1HsV*KK<{pxD&WT{Qpe>%}V@x+}#uwZy}M*G zDaP^t80cq}PvTq1P_x+mPXz?01qe%T2*F8w0*$ca-3z{wL?O?E3Z6sr_=O({;u_rp zmvl)XeQoFu3Pzy{@J*rhY>+(zY{=8rzxBecKo|gox{Vyt^Ogug*8aCTu@wj#V{xx$ zww3YT9+^2|`8TM&IjB9pNA~I*pg<-FsEzEwmU$ZxcGme`xS);sUihEjJpe%eNSN3i z3M7Mu^NAmXpSA;G_r32G*0w#k=%1MWzlC>vAtmkH_ZC7Wcq}}iBZCH=AcBu9bcR6S ze1svQ9e?|#)B%Kz4ZCk%cn8P5@PA?q&EYYz{s1KN+83vDk z3l7uo6^h5<{?9ac^gwvjvh^&e5D%oU^I?Ly(66e!f&NJa5DfqjdIapvg-!($2u2tC zgOen>fUw?0(7w|^z+HbGmk6y3EQKm$f-HB@+&7m2ULKtU>JyW@CO=Z>S^));K}5Sr z9~A0$17T+>@4JxH&2TRa0SLw4IrIi9EbvGen!Tk^BJ__#pY;G?59{xR#mjIYUOj&u z9RaPEYkH`ULx3bz5d4pC?w($5yqcJjnd$F^PB?C(gZHpoV`-#l5AuA!InePRzre|5_rkk341pWR0 zBIQSqQ7lrW*DEpArw+=aU#>|Rvxb;m@31t4Y*HLJBG}@!55@bQK;~|kh4YSphlVX3zu@Q(P70y0=R%+dDJ)$>k_nYVvb2AM7+mGyJYFWhSJ>2B|r$5#Dj=aoPR0X-|Rp=r&Jj+Vh6^%zjl*;i;CVi6vMeA=l``0!iqi zsVhfJYyILnHOgSNdL&swNbk%YGRiT>eI#GA3fU$D3LB%;Du#~x5GQ37&bO44$CAe| z(|a53TdIu>4VWCdvi8YLD}_A(J)%X8uy;zO524E>qrG&bswy`IKYcuML+WDo5U- z@n?Rpkdut(2^M}JFAK76T`mfh!j52*NR|+H?YVBQY3u#0%UfrghO3Ow*8!3VWXyq{ z`{whOWtfmo9Rce2)jNOpYG$oPACCXk+5m6Wk_GE$?Qg+Gu(aHypA&&c*uMyXE%A&r=S%F#ErTtQhIYnV z_TB_3N^0HZ-@3Hc1hUJs&fJvnR z#2(A*8%{jRuxp9SqvL_gP`F1WVkW^I_P@&oaJU$1R4S^46v#9IE#IGPT*oHp&^fGK zc*1~hS;%jo5v)oxjs!JNx)=TZ$Z%Tv9C*RFCDxs5v{&F)<~?}0vO)~8T{0_KyjHeZ z!)MF{)&j#mv5+wo86<=8rCGGb9Vq@RuCY4~{|y2IrKsG~lLAYx=*^{96P>I<4vOsQ z0d0ER2(MO7;qTE87-uXhxTzg6`4l&;$f`snw#g=7Ekr^sywD=wcqPgaR4mG`VlNZ| z_7)uCr=a-(b&~MmAX^w=`v~D5gg@cgpK*wFz?^kq5|4!m;e<6th_wliP^}5)K&UB* zJ%Wi@kQ?bCI%uI1yyDBs*Kx*&f22}K!wq}QMC80mjGKF=O-O@6$)yF39 zj7ISs_$9fKfZg3iHa8n6cE#o7=x9r6W$C@wEPugMV!utn)%sKSexcNDsabxBfoT+xbo^la?a?~&m4v+uRnq3>ZR&5*p1{_~z#{9Lj4y%fl{n|Puq zyYQJ2{lzEc_Eaje`+@>Id?toGWBhr|Sn{qAzf*V%b?)<|DeN8prIXpGeC80OCJdz1 z{Wd7LOSo1pTlG`68E!d8!lEk^iQ6{3gu*j{gz_eAE%wlyL^B$$!(&d`mro zYyUA!IQRy863ytSWTKu1WdOp~L!)0IE}FkcA0jsn2BVX)$Wm+GScbp{+PqA*`_KG}<|&e8dCw^|1C(1UzHdXaT*{QvBT-`)X*(H_Hnk@?XF5n=6Fm4%9{=UL` zyZC+$oCEx#@;cuBwX+fQ(@Bo-&Pdm>i$}b}F~j>7S=rDPW}fl(zsUro*ekBN>+r|EQS^zaj5DWJ!k)XE0-Pd%pWIcRSTH46J2 zg-I!gC2t?Qu(a!1T_BfmK`m$C$6IH{(7XR*H%6(qr1mk z%|eIU^2gJ@ny*M3l9d+5pNu4(t9o;shn(D}jk4$;OB;pRBf!Q6Y%v4{olf&hKZ!;2 z&X)3Y$t}X0be2-pRC7FY6UC%UmZ2+@k&&IG_e(kX+jI=J$iN;mx#Au z%BB*%#v(>SdB6ly=@18t%FLB(UlV9~^ww)zx*tvW4^whFSq`ZJ{M=SJthJFl<;KD- z>Nl{}V?0m|SQZ*sBF6R<;9+-(JwEUq*F)<<)9N_ML$bwoI8c1B_*&Jx z9pn^^0-)1n5D;X>qR)fP%jo<%m{SuON%izq+WuHPPXLqYKcX)*wxlRVPCy1!tiwXc zF)tb)`70`@w-<|`-RLhu%@<=csozQ{;-3ANB5nS?;N2*SSw1UkKy&S!9D>_x^b(6zo=TM<+2xkHZK}lW+F9-DGuJD%eH&;E}-B}PZ>(I{i{D#hpezvM+ z(l6T1G;EMozx2{q6?X9UwVPYo^$65lo+KdD*ojnKOIQ-j5bQ97y>8Y2eGtwhX*mo% zjt35znonjf>*rUT`&~E6m*nN=shz_=b<6aL%~d`rf27Mmd0?>Du&JvN=l>-va=}m2mj+GZ-m3wJT)`(d zNBn*q@6MD`AJd*IuOj!lb3TuBs#$q=5(+#zaa>+b+7Gr0b7HJ#CqrF9b6GP#Pim!R z-Xy%q%NYnGMBFs@gqxtPp0iGJw`J%i?{cUm5N8+z{ybnSRVUn6M*U<+L4Lqqy0?K# zILU#4XT=A4pq2ZEk0rpm^k28q*(uCdJs5jS9FzIBO`_ru9GlYo4b-CNGXAj`iUm}@ z6@KW_K%`b$Kr}C&o=|l{_u6T)8AD2mdxR};Ryp?3R`nD)mL^o?s$5Jf9$#kQ&nv+v zT(YxFq0ZXR(A!2(%+S!(I;PImc*npde$G>RYo2_)*B|d6k)>pK5O>b*Ds_%NB()I1 zZR$5)U#u)N$Q6QekmC3;ob+nZ3IL``rO>apeqC4nP<5%xh!iKtC$B4_xoIn6)|UmN zGS^}l3k=5~HD<7t3pt%ud!sYJ3k<<{Su1;T#ML|m|5ac@v58JqPD8n_!Bz5rBT)4~ zS5!-FvAt3C!0n$miF_AC(`Trpq}qJY&y$FT`A!ldnEwC`$&LIN#=>E!K?qbAV7&1X zqpH=vNKFXr)J`KBdY3&&K#mHEcQz-_uTuqjjqOuK6Z!GM(LsBLPHhrej*tnQEkcAZorg`mX;`n-8~=uYEK=a7}dWI*s(tn)El&s~zcHCs!y0p46bp=lPL%PM<`pnXq$KcM30 zrI0e}-;ZkeB^9sV;ia>(1y<|4CDT$bW{Lda)(dB7UpT@}+wI36aa=&D4!FQ)Ygkre z5NI&O(u>hT?^=60=RS84P=;3g>xa4HTui_e1v$J`XbhjOmf}fbE8$#UW^ADJjr9eV z+(b}eP5MNd{BbTnn)f!kG;3<3o95UrJM&6SbyRZ{M~PmO@I_~QhIWV56LFFK?T*?v zdA(@06kVCV%by&-p3i{!kDi-8q9}SV5XoB8z4eoKpsHjbkjGg&2`w)__4Xwqiy^&K zV&lw!50knmR2wJxUYI!0;xuDqKt*LjxXL&IUKY2Qb{t2-CTT2r@m&}H*NE_qopgt& z!7w%X1xS$n@|A;l^MAn=-*P5kb5>xT|vkrN6-YKU;aMBHTa*A49hz zyrR8pPx?JItRix(s&wj=8X9M~P_#QKa-J13_s5(Tgodanz!rxhZCAIYrrSG{Hs%Apig+>i5xkc!R=IO-r9s)n9Akn7 z`i2Be8O8XroK5-f1dBXbLE1U@%o6VLy&@zY$y@Lc=WL+wF9DE_Dw4m@=f-C(P(Y%= zamiDHAr}6?98bIw`4@2&CRw(vtwy1p{noVd&~vok^oti#W;L&X$|z5rF@xfHuo^GK zD$aqZJO#!&{mv_|h?Nv29kRHh1zgDs)aD|`6@4{D(`ALQ@-!q~9rmuRCLKgx=O$Kf z3>2z?32@*OsyD(DA*Gj!llKYibn>_l<8{EUWL^HPLGg0}(BE#N~>y_-gOM*bj{S8x;FG8+k zF1JP4PQc-&)u8eo$P1eaq#wS?ALBR757ZBc*;U|zd8d>h+_Qb)V&1Tn-e!(^GjEM0 z$g)^ZVzu|pGkf?6>D}C(@xqpe*&tF4*WLW5Vkssk)>N4IZZD5(+Zo;z{^7 zz&hHKCTo;*XY!;Wyv{9GXB4x%WeV?MR#i8R@hGe8h0^NKWw}8rr~Y^9WX)qU;-A){ ztTG_MWAh`+wPCS-g7^kO!gU*i(kj^X`2*vk0e*XpEb8TTlq=*;{zkltR~LMUl96rh z4VUx#g@@=QpBu7a>2HQr1B9t9<8Q}Jyk_pW*RPW91`RLD4_c+}(A?6g<-?~rWkVhX z1tt0o>U4kH6P9wg1nr>otx$~lkm32D;(XwMo<;p1lJrkz?7tQ$RfLxqOMlMQuRbvgPk9}qxZFc09eW7|?taL9p|H=Fot5?9gVhw}LEVD1ZzEUc zg+_Jtf3rR{Io`9b{lWN)f0FAVp~(B@PkZ^&cGeWTS#@Uf^oSi1Y8WD?iu6y0$d3V! zm3>pBKGg_46QFX7UZ~oCo^rZ^q%U;9N+og(7f7W#A=@BF!u~SS;_3xpO zA;)xS3HnA@izjKkbAjofoZA~cT}EMm_Qy%Utze$5fo(4xJ0N+PU3iZ72~h$et|%Fo z;ILjOMJH|2>ikvGcW9G9EimA^-63%apfQFCFz`_H^H`7c;6ZDVqUOgvMv$Y*4Up zb8*{z!xBRmG247bVbFrL;0+&Wck<8?fh-2w0nLUJN0Jo0ff3okSt1ApG;qxz*b7E zL7%F6RGxd$QXXLJHWHl2P?Vllgp%=K7bizBzphMQ%Q7#KRvfmc#}9(_ee@gZuQ4-8 z2iLU?OvJjN^UF7LJbeUSHH7rLkJRYZF!a}7!T-CLZu-Rv5BWdt&VV@ar0BmLbqgj4 z2-W}B-GNTZ8$kl5Xn7dn{l@vuA(L!$b_%z(*#89s%G+qhDk~*o$p= z6S(WC=zlc#iw1YN!ejs#s46NSuo-NpkJQ1B78wBAn4P*AcMAk^2H%fT*^dz@ zFYnJ%VeT1WJ4BkGBCRAm%&>Hehqg`LaqRcXhIWM3cP+J@8keuutgqjk2G|1WEgXI~it7(V!hz~%sq_fI%C@~C z-b%$F#XM=`sWl=x;P&vP*IfL_Y@6=3Uk^12t7cTS<7i}S+$mw3 zR?v>Z@?oQu3=55P@3l-5 z)7AlWX%kg#X4_f)?9<#!c%DIPeh5#W|gJE8~j*J*plDI3AXVu_DqPANIcT;gg9E41;p zML$W`!e(;?k=`OlPzwUm}SHQE(vpFmNIKBTCEWD2tczIZFSpZ z6u6~qb2m!^JLR;s{8pA(hI+;9xT)D7+rkq}OU^gcq|v=ToTrsNeEhI?&6g=%H{*cN z1A7YMm^79)emvB8D+J>a+v+u%1iG)ni66ztKbrz9^h>Rl1n33xBP27)sUm_%W-^IM z5kYw@gZCrV=6y*+;Bd#lkBG`7z~zl4+JB~HsXlpu?mv;QDQKuj>{bApT;yBa+ju#dU*&;QgiLUdiz)QcrP*R0QLPP?mONs;f$HCV-Q}x! zX>dAKXWM3ry6(xFm62?{qQ8YB=qz?qNQw@8-UOnmuI{9mE$~#mD{ukLJ{%)cgNM9U za^1((BC>p@&Ka!N?Y(L5I|E{jQ@FTkshFe4YM&^AXd!~;6*)gAtJY$x)S`hVQIR!k zr3dTSs>Vg-gZ&Hu!2!hzxDu3#*ixvq1 z@pQMeXHDn3%SiCFLr&*AU2aR5v-IHdO*(T{B!orn10OJyrfF*E2Bd*~4;neAgICfw z41NPXJoNtW5oG9QHA!t!0LVahSCP zlW^5VO}r9w?kx4iQ?{kyw@+>fy%T*}1s}EU<{~Ol&N95WZ(PF6=os+1y+kpitD@d0 zvm^G+QNv>Ga3s(o^NGWNg}~zYhhZMf;ceiKpxPGc8{;!pVcotg*LTH%;hP8cxAMJV zMYZ1=U^I#5YfOkLvT~W~5yKXj?NDJgpZn$Kr?p>RRcrj}l8JpN6omOKzB^s5>R;*j z4N0okkHz`|S0coB_cm>IkGoM^9m@zKTk_W=&uexOc_(tYeg5 zdj<#OvqPn!T6r%AR-1+ZMsQ`pg>%Y*S(y!@+HWNo(ppRsGk?08Ez6-^c$L6U7JHh6vaM<9qyW&0 z7J1TP7^_)XMd62Hd+9neCC6co)57k3E6CUMHVxDHCe;CM|BCIT%#_7C>WhwM17nSm zOMfTz2OdU@^9En?HmKgdYy?;?VxjYDYGYle&oua@@9ah8MbvfmF`ifXl=^Uo8}Ko?d^XHkD~pIgae`$qX@D)J6l&v_qEg zTRoWv^w|3msLq{ltgIe`Wh~M=8htfbblOim7+$rBVKyMChtWFS#i+@`e2^9s2V+7f z@&y8>0G&5_z{Le~oGD&N^U@4b-bV3hrf-Ke$sDm}O=Bv4%T@<3bC7qWf!IPB{=k9Q zAJkzqJIVQYDMDdWyqHdcKbsr3qN)c5gmXp{Y^gkX4Q4Fzrtb(Ea))Xjb?e)@=T|u= zH#s*qBRRkx)~z1(o1Ki=oeNP*11x7g!OiDxc|JqJOyQ}a*ZG5{qm-eA=MMO?aiBiR zpf%l~m*wy~!W|?RB39xF>5uwUL#!g?OPEJ*y&%M+zGO6pftj`NuKJ0Ab1+gz)>M%( zg-KG(^#0;CgHWKab8BXM^9y=4dz?u}67g%HF)r*4 zlU{*UGm$;*7~P5GWxHY8V#YpN}k!%+}PT9^n*B20BPE1*a zqC+aoxx%E0jO+n!R<2j38lE~Hb*KvG@C>elay5O_T$#Q?U+D$(X{&v{{HM7C-;)W* zh&5oyGE#ETv2_3fM*7-U`ZcBt}a~%|BWQi+R)-wcDbtveC*>5DKGaMcJhRMj{ zwV4keR8_-I`T0IjXM>?7+}z-tJ@_kVJ_Mk(BR`x%9aRx(N?Uu_sJ{uxzAksv4l`J{ zSANWk!D!zUG6@frOCq5*_>yX_;2G zrE76Q&%h)I-?~G=b~HW{v~>GvD4Vh`S9*hPJy#WP@R_xyep4hUp_ zY!NuwKzoFbsEm?Mm5^nPXzALh)54NzrMK7+yv(h<@?mb+iW~8it^g^)Kbd40ef%4n zicM{BF-#tsw$Jk*VfFG1L9PZSbTF3(p9_AOvj{O5!4V=~vJrCW3&3QcMmU6`TxM4Q zIzhsYq8V9PGGjW989h*0CxvZtw*rY2n4oJc@HUCi*)CIX)+bJr9Q+I z%D^c;V@$5fewq{8^1F%UqBUr#3Em%+VF~Aq} zlz~?d6d9haa6N!&51dX)1kF~`5%*v_{8GCsAoWJDvrIkx#gDydY{yUb(h#4?k^ zYN_joof)US$Xv1f?VsM;r{3GRaIA}@*eWvt>=f?R(5%QL zcRBV2wt3yV11|580JpKBto)1jn1Q>w+BK86_oJ2`Nao+dRuus&Cgqn9&6;t%i zZ_!ws6So#UZ`qM^hwyN{XCrAx`;1j@aJ@8Pbro+sy#)tZZ>d6mZxU{Hly>)niu}mJ zUJ)3qsa}#;0KR(y_%*q5LCHhuwgi zFC{|(AIN7(%%TshL*BM>iW0|sU1trY zMbrU^B&^(kY?+IMbX30FWefA@pN?((1Rmypo-6|&ynqLvaMiZlYBW7F`icsubt`7N zLNlbuXVL*BNaD)~1_UuUTlzdnY#mnOG0A$UN{OXb&l1wQ%te%{P{3;XWLEqrD4%Ih zJw>tN#vu;G0Gc851MGmK0J{-r3B^)6GJ$Lhb&&ZaUI~Pe?1n+W3Tal_crN zBrIDC9mnHA##8!5y_1*~9jODo`{F}2TL_=Ej?6ej!wOdv><}Cw{y7nUM*S1ISAg_k z3^U>JtuNhA>5`a6+tKKslcnWT_f#@zrbq$kXFv>ctKw-_7F*XcDbq8j0D#x}2Rf_O zl=g|T9ScE>xI}b4YEHw$PY&OLWUQ9#&2RFHs-jjD1?)Pyi6SKFT4RJmTvz<1_|Os( zut-!cO<$Wgj$Tpms2Jg_N5dEwSC`D>tg>?z#NCls3}3?`tts%ub zRUo9`(w)m^pm9CQrq9#zlYpPZ5aWGtsNw!9?ty1)=;c6C-+%+)Q(@@#9y&msL3o?7 zN}rCRe#5M3l`l6u!#nu=>frHELI4lR%2t0t& z>I0WdH0MJxugX|fl`uN|SRRcPSLSB(%0X>qYtVc9F{UHGEbac4pW~jDq_6|Xl&nUf zJ^wAtYLo=g_#;41Mm5-#RD{9JK13b;xlkD0zd>eC#pEZvD6z3-sF3w2jAWcxj1+Mu z7r1{IsfNt~Saefr=Q5ag?U>x-7SII#*f#Nal_W*KC|p9GEdr!TQLj9_B~vR553oR| zdYDf4yCL1{)9Ao55L6(Uc5jBx>2zV6X1bayt>a~{NeJQCdP6&MS_!k;TIXK@v~1Cd z%s5B3nXlHiTe48~{D-Q!pY%4Jx>6e%LRGyeJMgGe(uz}~ zF%2?6|5W)LUPfQK*M_(x{~;!3@Q%6pH0==;Ltk>^*!Mus*vslzQ#KMK`_CZp=(uL6+~$ZhS$j}w z5w%~~pBy}=to5;f-Z^Qy0Zxa~&Nt0KHnszAjPr+&#EVl=J#RL(tUlKgswgwuVijp0 zUDhEGZIseh+32mNhh4n}q)OYJ-rwIRufbYujcX)Vyd|9{Z5zB4!5!bmz2lWH0g*-ZJCJg7nx?fV z{``b3>9*m((7Ek*UF5qtnpiW5_aDP4+P4zz%pYtQs2Q`Y1B8s_)UN*aMubf-cgopW zzd6VeWw6-VY8pJQ&cSil(prk`l8|BOsj)wubwqvP(yR`}T=ko%u`bBYJ<`3OUW*E; za~mk49As9-{A_K4240&;Vt7<1PRgCun;v(jRjr>T5!4pu!Sn9!a%2;c=W(!Uy@1}j z-*5Gtfnu}F6rJPfh10Kr8o_>I4^TlMtu@KaB4bV;e`~1$a5ph5+cLIgNQd$}jf+M*p-vKH>$Y=o%e1iRq`$x(ns0JBj1ry+&0D#{6C^X*3m z0_UFdmaM337rvA0m@!|DA@a?>jcsCUt}ljor56ZKa{DZgm5 zF~;h`%AfQ6qHy8;_MEhC>5XToW)jdOOID7~euIZYU=!74I}>W^U01?O=GaFB$8M-L zw^cz!qwF=LEc^vy30Hqg`H1@3FMHjT6eJ9lpNM}nhc(1g<|gTe?AE0!3bS-_`pO~? zO4^7GDs63kp`7!Hd8F3vU~&a=_wDUtcK6`Nqcr`hCEE%cX1nAgK z7}-E!0nHqppwg~-h<+D%>G3QGL`^9qjUD%1ah+0CNf#$nU6y!H6pDfrARANUfoK|S&Nx_2ArYQ2qGa)FVW1JT*W z=&?c}gA&|-?!4#4AUvY$mTe(xUL0JO>~+oX{16mMo>TJO?WrUD1)`+(Cyyp;!DYs@ zJi?y&MoSJR5=(t6t6CR!F*w3p4}wyKDIRyT>QYvDTQ$wLCx!iqH@art$b8Dawm3ZB zroszq-ko+&*!6q~!KQBt4#G|>52Y42&ZLpnhA+||EWTT@GW3@!>??TGc}q(8Dx_Q+ zRl!@}|CO0HKAxSp{~Fj95R&2`aFgzap_1fBVSxV%;F{=~7~lLXB+Su~R65mEHO-+^ z!!0#|RA4&RVvvw6t!`4e#;)lRNwDGD9$jl2J!^g~?&7ql&)K=Thi(4%rr(iY@OHN| zKZHbz#}bEhZ+dQD{XM*HXNZ7?e-Qg|^>Raat`5xwIr+Dqpil-uiIAX_B1HxG@P83O zdjg%)CF9=isb1q6j$k-(IsLZ8z|TacktdRhnouPBh6Sd#6TuO-hqkl zZ_wu~?#~`)wo`(?)JQ*#0W-5jUrJKQvIoLX&&}BOFdmY~_Q&M^;&-IEJ_&yJjN)?Iyup{mh{ z=F7lakYCThJxh8KsEPBd3a_u?&#Zamqg6|<1c6vaT1s}WDj-iv{2^;$q}CNWP78#P zm`#Jegfqo-ghE3{eIDj=B=@fA2V0E?vk2zJx3Y;fYkL1>PL$SaBe0pwl4{!&DmS!f z0(OAH>{2Zzh9nc@EHph?)-OhdH9gWwCuwBd8#WY?oyHC+oB2S^$r|;Nq@1yrP2uO6 zJAU%X!p2Itv+W@#;Z>%+?Y*^}oeY#Or(Z%Wy+Uu9ww!D&ISH|+W55LtI#8mUB25k>y#GM=R@Aqsbs2wM!i}Q7;cVi@?T^M3U*(vp zD+)}D(+2kg#J?(2tgJ=cX!927{X(}YUVI4o#Yw6IV3gzhY8kq$s;q8Y1DyVeH94NOq(w4DS2|-?!$FH4oYfXLWQ1!F8hKKEGndeXGs^X@`62wQSvTsL`4gmi=&2@ zGE%t1dj1#prPoU0_A-*jg|g-z(@nt3>cbR$$ZX&fi&W5ldP=cG{J41zEnPF-ZPp_8 zW#hY4zuxnMouATB1n6_QZt?5p#z_DEw7{e@d2qXqocCBdP-$8&t*9f`YGoH`hFYNZ zbR_2c5B-T{rD_p>ME8EH)%}DAW1<50FzNFAscbmmMT*ko)cXr`xx@I&6Vy*{Z`?ER z8PV^{`rK;H`Eo?^JC#1JW@iM_W@`lUf)8d-5%o#zJ-kBi)BLvfPYZ?LRnnq!(&xM(A?xK!BB*hTpr z>Noaa!(;LThL9^L^&O|J-#%Y?c&c=_?itDLt$?ZRiZVy)tadu8Gu(rjd;YtU{s)@1jFF>}EdxVl(FsoW)bbBQ*he8~n zaTJVEmDVrJ2AEATUY!wv<|uz#EXsOOghDb^5d9}m|IEZ)Iq{PMb=vw634_mwnkSop zLGpO|xcs#!yJ7$rUFAY1b!XPoV4Eo~UoamsuT<6EfUR*yC(@u__zyT~_tZ9}2=Og6 zG6%8JPm%5dw!k9ROnk$i|IEAAwNCkTPp?gFXjqEWt4|KPt)UDzRE} zmCkZ4kzQ@L>s+g#*Aq#W-7YVTv=KkUL5IsN_bc6F?`dww_s85eGD!43 z5zhw+DfVz7eyS_d6fW?AhheWUG7t$MPJn4kK;HcWTJpxp&6BFCz4)LpmP2m=bwr&Y zZ|Jn-10J`x{7|-?B8>6!pEH0P_|3VES8!Qp_0}1ic8^FTyj!L6P#NlA z<0(Jn!RD(u^vd>c+jvRipMMzUDLRrx<6nF*2AAJ2rJd?X0S7F-gwqr@{HVN)IEXe) z3qCv!nOaY&qMY1mtnc<+d9sxhb~S&QW^GD3e|nFwI4xDARZ@X-ms?A{%t+;Qz?>`; zFRjdX^SxQFm{@;2UtX7vL~5b6WR)!md`fPsv1+2i7%pW~i!YHh9k6LC;>Y8-w>7mTRpNI^^SzF8ucAp;| zchVquPk9yAdrZ2gN~GblYN8aS5tNc5UJ zG-aH)qPS{wl^M({((JmTKC3-;DZNUWBrL!(PuK|>4u8MdKucjaiKv!KE-0^~sawiZjnI|)fiO4;aaD6izXsPvXzzmWh)*tRKq z4o)yEX>Ty_YLgV0hd5~Thd-GTM>v5?IxhF4J9B{sV(PziFG@*kgiPy>C?b9$2x>Zh zf&yB;zlnHo?8HYH5xPU#)IS&d>+Wp8|G5H)*}ze0Kp*QJQf(6(=RW>x2S$g4j+CV7 zGh5})U%ONL$TVM;d-|Uf{o2`N5D)yJeqd^!>-~m~)BT1{q9gL)xBCuI2L1FJ!{L#e zP&8WVPGkMMPOJU68i>`hTE2u54C>mhH`tONaRJE?;I$wQfcB4@UHMoc`{3xC57jTf(Seyf~17LB>6>Za;1uLhr>MOH~wW ztmhsjP-?t2mFsSd& z(N0JIsK(UThX!*n+oLR1{?Kf;)*yf=tilmv>$*DPcP?hJq&Wua_)0?dno;T2>jN8F zNd3Z&92VRoDTeqLer$DATtioC`yHU}=zVyVV2hx1d2xG3KMr*_LEI_V3a-@q<1?>w zamP*UOjv|>{)lz*O{7io@zfVO?Wai&7}$hCK-iXm*Br>|4JzYu7u|_?VOlc~yJKQh z`g2x`HAa2C%`~?3)(*70cp9Sqk^@2^+>QC`BYH%3mDJK=b_U(?i>>CHC#zd}pj3=S zeFzYRg#LYUo{uxNT~j}#O7EAxNfkJ4*S>s}!ose;x@KC{V}V+D(ZR9tc`)Clrl~R--Y-6>h7?ZnjOw_3+FpMQm63pSS!QRi}$m}tE zr^drrllfs(_skc$%ak8*buWGpE`YzP<1Rt7=JU7jwfJTq?qx*uHGf1D@PxE0`t1lA zrzdH+Xj!RI!ej62nRJUb$PTQ&x}*{|8Iw9w zUi@IYw8-|Hp1(*MW|f@~Cp}m3j*;pWp)(|ltwSH-jM#%SyH$E&Fyo}^5?{y7ea)a^ zmZ7~pQ`HN@ITn{=4ZUD>2}NbIMmj{VK8NWM4ACwwkcG0|s#$HY0bH8;2@v@tAG+EKj{RhE7ZMO@2zo$` zB}hbOPqkd#kpz~8Xau-YaHY=%9~8J!;3Jo@TImu_|M~pI ziohoevv_8-*p>bwP-^HKFOB?XFHlFY;Q~JFj{55e$L6?K@sCAykb%IEHvF|J*PM2| z!RWpAtjLpF^ok_xEfcK%-9BPGTZUw;>(mKKzCDWNYB}DpWd1ku|7GG`5q?0@nV}@k z-4vrL6N@qvTRjVN8w-bqqdTH96AKgDev*_CHR9I4qalbhPW^NL$_oTZ{zfFg8T(;k zgy7q5nHCsy5qD%g?<%$H67{+i5{fArA&4Irhn#Xus1d>n>|_uxeIzOloDEva@4^-^W$*ibU*^z;MU1k zQ<)NRtiS~T_!b8pa&*ga!LmJiN9KI9DQf`E&q5VZ1*o_Fk+yic)yJ7n!o`m!Mxgf? zIaZiImt1l=#8_%#-BG3&M*Trmmr*cX5tqCUG^)=r>|MwALjicp!Y zl4_;Xie(e+6<$*q?6aOKt~>oZeQol-Lj5n(z9KtsdsxxXaIHTAI!<@EH~-|i%!LC# zPvd}4jtG6fm%@d!o!c)6wos#WpfKuI8Mig$!$ycX3FXFJt#82F^n}$wsHDYcE zb(0YW+EDFY2fxk+yFZQR-pp70%vYUnN9n#a(B9{F%azRY@JIEfRtgYJu{1S?oy^ds z#@T;Ei=OH%y+sBvRKr~w*i_>%)@`?5p8Sv-G?;vy{6IcNl{)ijvOXzj#iiclxoDux zOfy1VWs;kO3V-;t)pC)p-gMwC`FmxrsZ=)QkFFvZn2h8JV3nC7Tg5Kq=FHL~X+2pT zkPFp{Z+A$;VXjW+p$8o3l}j4LS#^yLY1GCRs3Ue>WXL4dF3hN?p-Z1-J;(_hPVTWx zp2LeJfB7;FlVlc!329A-6IuBcxKn!dlWEocO~Ekb$qRyd{3==hpx!S_RM#%Bnd~M} z9dX|7QBnVQZCVv)I0@v02JVXHr5Zl+JW-dAcOhL!zV+3(h8mFWX5;zmW(ENoAPnp^ z?>Z4Hc?-6KEewhx`iWEZk*ls)4tHa7V?CoDZ_Uc3+%ze!nyk@MSw>$bLw{UYZi~|0 zk3_K9U~kk*AS=E50$>w${-KEEYMcyIO(q)@gI?dj+)jm97 zWCpq3kF}Z4hYQ4K#XcROuU;;=`s{p4mU~@JB}puAlX=AE-g0k8{G~6^K*gWSImzyV zSG%6Iz<%oo{d^fLh?np4YVa+%cM;oRQkU`U8Dtkk-p}ju+x$AS$uhTf*iNCfg9ugn z(i7VuVO~CwBIW{35R)gCc=aVvR9})1nQE=+-DJ=y+6_2R6AOJ$2ryi_ck>k;;dJwb z*L3TSaiEpjr8mR0u?RkUL+Y(M@cFd2Ezuu&b^8PnRCAms@+mqWux8l)7(lh4A3eoP zU9aPrHIW&MyEFAyXw87g{_bdw&D$%(hY;nN-Ge0{>JM}KM8Ko*uRRRnsyoDbBOh~F z>NyRe!T}z8LzUIiWeS}9B`kfKH*%xWZ`gKS=$0!UK~+wN1oSOBA>#4)$H4RW2f<&9 zwKfzJXQLh~HqQ~vYD|6WHIV7tNGw^`95+>p(Y4{?NDV&W}M| zT0$HDQ&L}boNqRFVv53@5NENG)0PKej?!hzOACbZ<3n@gW&Gh`-DgY#7P zvU}sp1=X;bw$>Q-$p@SX2Fxc2iXtElf*>p?I?%|yMS*j}g(+em9EI8FjWKXEcF_z0+q z!+*!otap%$*Y3fP#zmZ_)@+4ExZD2tx`p`E*A%jg@J)sr`ld-v)2s+t(J=4~|s8zr(2RT@DZH7G<^0!r=#h-Xb%1+?;Ri)&svepia<( zP}glF#Vodm_xp}GHfc#vXYb+k^7-a|Fi!kf${i{V=JW6L%orYlQTE52dzU*VcM(!+ zzFXPxtF7=T>J>%)=fknp?-hAY*A+5kql_rQxZK)P?~(e{uc-<3B;?O}K;jg^EWK*o z!>{cQPDP)^WoNVFgmQIeILM_z=pS_cI2G^Fd72MjVzdY$C1=JGS>BAx*qZ0 zl3b$iQkK9&0%;a`ltItGk;u0{H@NHkVORbC`VRl0?52gt`TwQnlIY6+(l`~YzPcJ3 z2uKCZKguOex^sp4U&_tVhV@nZ-SOQ!?QCJsfI&Sy6JG3VCz^4s_HXeKU?S3%6>ZHD+7nE#zNmv{V#_5P_|)CYNf`_!Ny zz*DehNFAm zIuW8I+UkUa)uI!kMGGQ9lxSH*CxnO~78}Hhx_a~yy{}I69;{BZ=p}k2N|66pzAr!D zf6kdbGk2c*yw82_otZuJ-uoWLv$JYqOAu@9?qT#nQ3uWmoH5UrV?og?TCH^7G_1N8 zG(bFa~9=g zk-GD2;t8zUx;cYy_)FhKTX{r1q#|g3dXKfCyOr}5zmtoXOBWA#!6~?RdH2nLe)i7c zAu^E{O0dlt^gUyONx$IQHYm*9&WkrfUo)Zt!P-o$S+&FS~WG9=&vK`6cvis zL5i_$Sz%61JNHY9ZNLj_d}-XziYBdtTJwPil&KZ=&?f)9_Q>Z1)k8gOB_h_rr>dEG zP@_`&#Wg=};U}4s2fvX zXy+hAu%8@*Y!P$TtK+lps)S4$=T2K^Ek&y$IyE(_6Fc>(Y>Myey&pym7Z1Y=uglt& zagNw3jC|ZFI(EUU>-@#n5_5E=dOR^~LuKHl7)z%oZWT=R^FzHR!Fsd#XW*zM`wZwo zg_$kEwZk54sp8_Rw8;zbsUB@kCP*1B8;us2Ilxl1c7uqmXrA|zfIqTrPi7pO#}oY3 z=Yw~R4hzx*2$*))yyvwl9KY{WB)qh~amQEA$W@%5ia490+3SQVcva=F z+8aY|=UtBxRE`B$QuuC8^ZZyyZPKc#Y!(97^{UWbgbzWLWc+k< zo34Znl}@eU*{%z+8jcNF$RivN>zeEXOTyk%gQA`k7o_7P=f3NZj+dif6YVf}d}vKA z$Xfc&&*3vcp7+QgWYat@%0^~%J0UR*rQAVTl|Ae7rLrJVWN&Aoy0?a#QsEspPp7Ui zRp9+R^F_ysP!~)vWAGKnm+axt{OM|9Rh<1HGnNhbq~tgFyGS0hWBr<^`mML-@$HZX zJsmFj8u!eSwy=q)q6192Cxya0A)G(61;i8J)#H=4Y~+K>J&8SnX=awBx8*TIE8KrI zbm5llewfXp(?X`}a^0Xj>(&%irj_ZA8>5;=p z{Ra!E~#&+96wKL!Ppu1&Wm6JvM2TmUJvdJX_-@YH>ed=Ns>EWt;Uf4H}la)8= zx*FcIh&R(QO5<^T#&(J@|1Sshhq-u@-JgkMrH1qZBIdu*jgKfyFKn zmAcSYDhIq`jO^1PJ_Sid4GcE^SaHc^qmTaK-zl+wYQ8;xd_Rh_Y^QRZJJYvKorRAR zPJD!WGVm?4#Y_DM87v7pdrQ!BBsppOG0gpCR3-^2%$;qnR)Aa}RnOa^xwNtU;3@3) zftW47+?#Ah?}*zxQMhCECRi5L z>moq-YHoIEZppL9*SW>R?=|J%G#YsYF+xw-N8`hM^(Vz4INXz9&hg3?oZ*fi-n!nI#oKFLX8Lg$dq zRaSXfxlsSNRL(GiJx5=05fQxi?m*D4t( zn`B__Pe^HE3y9c`R>Xw$nwyyYWEXI!);W#OB<7~GD|YFgnz9-ZrrRgIuRyv0P6%LIiv6yNlJ0 zVx010GVs?ZTO9}?_Ck9qaj|8WH!hq%qm3}EZ;+jBY!zx>yi7Sp^DM<$gyfn%j;6lE z$Z!=i*KbO5S(t>4(qs>v_>TmF9+{}2l&q!Wu{>e&L!1Md>Xk>gPel2|!R;H0QDNf* zmb81e;C|pEo6(;5VyuaGpqif*+aH%){aA~I2D3VjksW2}CSJ^+CaIQKW1e*OlsNL#@VyrON*&N%v^f)#@3 z?>Qe-$5J96FIt>zFa_B-zLH~nrsY3SM0Q{;|1qD7cD(!hhJ2ub=xF9aP}ethC>7Qx zq;7!2q_whfB@SjaUy2Z`!7%ah_3-IMUPfooMlw4KbaxOZJG{ReK`VY|>eErhycEy( z9H?}e@5|_0oYcJ0vd0X#PYrr@PRKpEnR(r)xxe2s9wgh{40t!AgwPUyjU7b=UG4X%9NBWk=sT}nN&Z(>I`9a|qDl2oq_T)kBFioD1m9~sq z3`65FWKaHk;kQ3>d$-@b%U;Gfyv=A^d!mIJQcwHXmB^;fNWuSefTK?E)8{03To-3$ zbenL*e!r-JV=eOw#vlvvju*6YC23?}6=>C>+y6CFxRl=*)eK9DKSk9azr6#gS5J{iP@+ca1M7p@IiA(q7kR{41tBu#% z|M)Fb5KMw8pzV5?Kp*M6m)KmRpcxS-S5;V&l+$48G8@kj2X1k4Um>!}9LACLW)T0u zMB#_U@rs#nimCYr$YZ!MH902lNaiU<{4e5x27 z766^F0>9D}FjVGtX4UH`(!Q@pE2*-vNAA0N7a8P9n6u9z%lH^SA^CPlGIdDW!^Hb{ zD`Vyi9w;ZO8%`?SeZ#sjXE)T7;4HS40Y1R{+8ly1@+7Jc8MtS#S~#m?B~>psy5sLG z3H=jnKPQFaKGvy@l=?LB)bfvXEVuz;Ed5#E0k4eqY8w!~HKS6^;$ULJ7; zM+#YcL4qZx!xK$O>wA_YAf@ECH^^D$`_%7LVW?^|mkNDU;k)5nnHQai-CymcRJQLK z2O_M5s4@=2#vyV1e3FjadjX2F-c5d{NKlRRJE5juPr7t!Ny4-%DY2&?@ulzgLwy-i zl7hTKVX2RuDOZoli5^sU4mhO`!aaP{hubX;%SdLKRd_(P=>m0Tb-F3Q%V*L0hbEYi z_t$4>H8sNI$7{mBPztw^MJyS8>{%K(s0jnt?EDDcz}n954oIrRN-{|0;;J)&QE&r! z=oJ=kIi*<*B@wDCwRjK+i(Q%$bkI;qv$Nzk)3D>a>3XIHPd~mRjM-iNcPL|0LB>J- zyuk^tmKRQ#b-Sc_JGK7RU~5$$@kq**w@IE}p(MNAdvZr);Y=-k10l@b{!6=4p}(|u zWlO16ZgKY9wCcTY(|tIIjF-aB`s#i{XDS>bG9WRn_;hX8XMISjUizxo%CToPQLo_l zI+bo?R_N^4C(qv^g-ZESXdXf#8uK5W-}_LA{`e@O)d3l0kt7LS^xw`C!r&>rX(!L1 z#Nr;@QjoT)g8{6tM0sE&AF6Bl{PFGD7)AQr)0);LwBmeZe*!+Cb-tRi<#jK(%NRnR zJ&j+K-_ZKmc$V?C;MPviy}G*dFB1`1NBij8G3ji+8uS#}&sPku}E-~;PgOphEM0dhH|UPq<| z-{rG`Obr!&69L7jQM}*aAj(sMc%z-(vYzHNGQI;$z@&LPAx58zf?KpSO}D>XTVMO& z)UWu5m8kdty841|8Kil^*vY=0UNYzArUG^T) z>26A@8ubZiX~&2vbXwfrMdmQ$!QWXgba?EYrn?*w=y6kv!K)n%uQ&F})zIpc+asgX z41<3WBHdA&xFLPXbsC3`_!~dgoQ1yH@ZF=?@I)rbyK^rEqCBr{ys|9ZU+O2elp;Bz z?VrY}{z;eq`lJzx@rd}}xiaW)R;MZ3SY&5Miv8y6;XuGC9v1^c0FhG2S;~=-brEwK z&Md<@k0k}?VO_+RA_})IuKJf4TzvLgE|tE(fh+qi%1)~^FK|z4!?6j@Obv`@Y$CSm zgwK-60l`8HKrea&vd&#>q!=eM@xbxf zGNnfQkfwfB)c{>rbji9>?7Z=ahF#SKw8O=Ye*UwJS+ZpTkv#CUsod4mp^f5C} z12DtD&Uy|;|02B^5JiprjQ>B!=U))g8w-Kiq{(!KSzgz5$^kA-06WWH2nN#I4F_?j zjS>NG0-=F8*=|z?fWpT>o4GFm&2Z7VA2(a^5Hn3oXfWNJ;Y2IYp+|rd1TKNE5}?6i z1WbshEktPW$gnv6b`ldq^ zw}Y^bbPzr5^ylZBz&VF5g0Ghbfr5Y#k{v|n!X2>p3@;t{vjal1YxBbq07Cx@DX~Jl!(nQL};QFR^nYnK&1*mcIOgN zFa)SThbZb~MFXX%EdR6vL1us8?vcC%ylIMNrs(281Lvbc3V4F^ViEHJ^3{Sx?-BUQTk812kFNYTVDMinR=xo6HX@+!T)Q#Jy@52qD;$7V zF5lXiF=)on9!50dr3d>ZIX~n6Z6rqq5!e4dF3$$1EBl<*GvFx$x{85u_fo~N1Yqzf zpAWjXy&$ZxRKNi||GKrjp~xu)kd_1V?ByX4o$;?lx;_@PyjwU*L^Oc6EY8|;$qGCR z0Mj9e0!hw1h6c;^Q!cFmZuI~pT^`PB*=Xj`zT0RZ0^@G%Ssxq&tYVi~c?uAWD8+N_ z)+i7bW)Zp~Pc`}37&&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa6..5f192121eb 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From c8faee4c6200337839fdfa9cdc378df36bf4f966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 11 Feb 2016 00:13:17 +0100 Subject: [PATCH 519/857] 1.x: Fix Completable using JDK 7 suppressed exceptions feature --- src/main/java/rx/Completable.java | 19 +++++------- .../CompletableOnSubscribeMerge.java | 23 ++++++++------- src/test/java/rx/CompletableTest.java | 29 ++++++++++--------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index 003f21d8d0..971763cc46 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -938,8 +938,7 @@ public void onError(Throwable e) { try { disposer.call(resource); } catch (Throwable ex) { - ex.addSuppressed(e); - e = ex; + e = new CompositeException(Arrays.asList(e, ex)); } } } @@ -1298,8 +1297,7 @@ public void onError(Throwable e) { try { onError.call(e); } catch (Throwable ex) { - ex.addSuppressed(e); - e = ex; + e = new CompositeException(Arrays.asList(e, ex)); } s.onError(e); @@ -1619,8 +1617,7 @@ public void onError(Throwable e) { try { b = predicate.call(e); } catch (Throwable ex) { - e.addSuppressed(ex); - s.onError(e); + e = new CompositeException(Arrays.asList(e, ex)); return; } @@ -1669,15 +1666,15 @@ public void onError(Throwable e) { try { c = errorMapper.call(e); } catch (Throwable ex) { - ex.addSuppressed(e); - s.onError(ex); + e = new CompositeException(Arrays.asList(e, ex)); + s.onError(e); return; } if (c == null) { NullPointerException npe = new NullPointerException("The completable returned is null"); - npe.addSuppressed(e); - s.onError(npe); + e = new CompositeException(Arrays.asList(e, npe)); + s.onError(e); return; } @@ -1900,7 +1897,7 @@ public void onError(Throwable e) { try { onError.call(e); } catch (Throwable ex) { - e.addSuppressed(ex); + e = new CompositeException(Arrays.asList(e, ex)); ERROR_HANDLER.handleError(e); } } diff --git a/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java b/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java index 2b1a3ad2f0..a1c3cf64e9 100644 --- a/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java +++ b/src/main/java/rx/internal/operators/CompletableOnSubscribeMerge.java @@ -16,12 +16,14 @@ package rx.internal.operators; -import java.util.Queue; +import java.util.*; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.*; import rx.*; import rx.Completable.*; +import rx.exceptions.CompositeException; +import rx.Observable; import rx.plugins.RxJavaPlugins; import rx.subscriptions.CompositeSubscription; @@ -197,19 +199,18 @@ void terminate() { * @return the Throwable containing all other Throwables as suppressed */ public static Throwable collectErrors(Queue q) { - Throwable ex = null; + List list = new ArrayList(); Throwable t; - int count = 0; while ((t = q.poll()) != null) { - if (count == 0) { - ex = t; - } else { - ex.addSuppressed(t); - } - - count++; + list.add(t); + } + if (list.isEmpty()) { + return null; + } + if (list.size() == 1) { + return list.get(0); } - return ex; + return new CompositeException(list); } } \ No newline at end of file diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index e46eff8423..6f60c57347 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -1872,10 +1872,11 @@ public void doOnErrorThrows() { try { c.await(); - } catch (IllegalStateException ex) { - Throwable[] a = ex.getSuppressed(); - Assert.assertEquals(1, a.length); - Assert.assertTrue(a[0] instanceof TestException); + } catch (CompositeException ex) { + List a = ex.getExceptions(); + Assert.assertEquals(2, a.size()); + Assert.assertTrue(a.get(0) instanceof TestException); + Assert.assertTrue(a.get(1) instanceof IllegalStateException); } } @@ -2217,11 +2218,11 @@ public Completable call(Throwable e) { try { c.await(); Assert.fail("Did not throw an exception"); - } catch (NullPointerException ex) { - Throwable[] a = ex.getSuppressed(); - - Assert.assertEquals(1, a.length); - Assert.assertTrue(a[0] instanceof TestException); + } catch (CompositeException ex) { + List a = ex.getExceptions(); + Assert.assertEquals(2, a.size()); + Assert.assertTrue(a.get(0) instanceof TestException); + Assert.assertTrue(a.get(1) instanceof NullPointerException); } } @@ -2235,11 +2236,11 @@ public void onErrorResumeNextFunctionThrows() { try { c.await(); Assert.fail("Did not throw an exception"); - } catch (TestException ex) { - Throwable[] a = ex.getSuppressed(); - - Assert.assertEquals(1, a.length); - Assert.assertTrue(a[0] instanceof TestException); + } catch (CompositeException ex) { + List a = ex.getExceptions(); + Assert.assertEquals(2, a.size()); + Assert.assertTrue(a.get(0) instanceof TestException); + Assert.assertTrue(a.get(1) instanceof TestException); } } From 05bd02effb1a4a6f1063090666d82af5d228b5e7 Mon Sep 17 00:00:00 2001 From: adam-arold Date: Mon, 18 Jan 2016 13:03:18 +0100 Subject: [PATCH 520/857] #3618 adding source links for @Beta and @Experimental --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index aca2675593..789eb54959 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,15 @@ Version 1.x is now a stable API and will be supported for several years. Minor 1.x increments (such as 1.1, 1.2, etc) will occur when non-trivial new functionality is added or significant enhancements or bug fixes occur that may have behavioral changes that may affect some edge cases (such as dependence on behavior resulting from a bug). An example of an enhancement that would classify as this is adding reactive pull backpressure support to an operator that previously did not support it. This should be backwards compatible but does behave differently. -Patch 1.x.y increments (such as 1.0.0 -> 1.0.1, 1.3.1 -> 1.3.2, etc) will occur for bug fixes and trivial functionality (like adding a method overload). New functionality marked with an `@Beta` or `@Experimental` annotation can also be added in patch releases to allow rapid exploration and iteration of unstable new functionality. +Patch 1.x.y increments (such as 1.0.0 -> 1.0.1, 1.3.1 -> 1.3.2, etc) will occur for bug fixes and trivial functionality (like adding a method overload). New functionality marked with an [`@Beta`][beta source link] or [`@Experimental`][experimental source link] annotation can also be added in patch releases to allow rapid exploration and iteration of unstable new functionality. #### @Beta -APIs marked with the `@Beta` annotation at the class or method level are subject to change. They can be modified in any way, or even removed in any major or minor release but not in a patch release. If your code is a library itself (i.e. it is used on the CLASSPATH of users outside your own control), you should not use beta APIs, unless you repackage them (e.g. using ProGuard, shading, etc). +APIs marked with the [`@Beta`][beta source link] annotation at the class or method level are subject to change. They can be modified in any way, or even removed in any major or minor release but not in a patch release. If your code is a library itself (i.e. it is used on the CLASSPATH of users outside your own control), you should not use beta APIs, unless you repackage them (e.g. using ProGuard, shading, etc). #### @Experimental -APIs marked with the `@Experimental` annotation at the class or method level will almost certainly change. They can be modified in any way, or even removed in any major, minor or, patch release. You should not use or rely on them in any production code. They are purely to allow broad testing and feedback. +APIs marked with the [`@Experimental`][experimental source link] annotation at the class or method level will almost certainly change. They can be modified in any way, or even removed in any major, minor or, patch release. You should not use or rely on them in any production code. They are purely to allow broad testing and feedback. #### @Deprecated @@ -124,3 +124,6 @@ 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. + +[beta source link]: https://github.com/ReactiveX/RxJava/blob/master/src/main/java/rx/annotations/Beta.java +[experimental source link]: https://github.com/ReactiveX/RxJava/blob/master/src/main/java/rx/annotations/Experimental.java From 3a87dcb304b4fe68fb31c5ed727eafc9f780b9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 11 Feb 2016 11:43:35 +0100 Subject: [PATCH 521/857] 1.x: change publish(Func1) to use a dedicated subject-like dispatcher --- .../OnSubscribePublishMulticast.java | 484 ++++++++++++++++++ .../internal/operators/OperatorPublish.java | 41 +- .../OperatorPublishFunctionTest.java | 255 +++++++++ 3 files changed, 773 insertions(+), 7 deletions(-) create mode 100644 src/main/java/rx/internal/operators/OnSubscribePublishMulticast.java create mode 100644 src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java diff --git a/src/main/java/rx/internal/operators/OnSubscribePublishMulticast.java b/src/main/java/rx/internal/operators/OnSubscribePublishMulticast.java new file mode 100644 index 0000000000..c1d14b29fa --- /dev/null +++ b/src/main/java/rx/internal/operators/OnSubscribePublishMulticast.java @@ -0,0 +1,484 @@ +/** + * 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.exceptions.MissingBackpressureException; +import rx.internal.util.atomic.SpscAtomicArrayQueue; +import rx.internal.util.unsafe.*; + +/** + * Multicasts notifications coming through its input Subscriber view to its + * client Subscribers via lockstep backpressure mode. + * + *

    The difference between this class and OperatorPublish is that this + * class doesn't consume the upstream if there are no child subscribers but + * waits for them to show up. Plus if the upstream source terminates, late + * subscribers will be immediately terminated with the same terminal event + * unlike OperatorPublish which just waits for the next connection. + * + *

    The class extends AtomicInteger which is the work-in-progress gate + * for the drain-loop serializing subscriptions and child request changes. + * + * @param the input and output type + */ +public final class OnSubscribePublishMulticast extends AtomicInteger +implements Observable.OnSubscribe, Observer, Subscription { + /** */ + private static final long serialVersionUID = -3741892510772238743L; + /** + * The prefetch queue holding onto a fixed amount of items until all + * all child subscribers have requested something. + */ + final Queue queue; + /** + * The number of items to prefetch from the upstreams source. + */ + final int prefetch; + + /** + * Delays the error delivery to happen only after all values have been consumed. + */ + final boolean delayError; + /** + * The subscriber that can be 'connected' to the upstream source. + */ + final ParentSubscriber parent; + /** Indicates the upstream has completed. */ + volatile boolean done; + /** + * Holds onto the upstream's exception if done is true and this field is non-null. + *

    This field must be read after done or if subscribers == TERMINATED to + * establish a proper happens-before. + */ + Throwable error; + + /** + * Holds the upstream producer if any, set through the parent subscriber. + */ + volatile Producer producer; + /** + * A copy-on-write array of currently subscribed child subscribers' wrapper structure. + */ + volatile PublishProducer[] subscribers; + + /** + * Represents an empty array of subscriber wrapper, + * helps avoid allocating an empty array all the time. + */ + static final PublishProducer[] EMPTY = new PublishProducer[0]; + + /** + * Represents a final state for this class that prevents new subscribers + * from subscribing to it. + */ + static final PublishProducer[] TERMINATED = new PublishProducer[0]; + + /** + * Constructor, initializes the fields + * @param prefetch the prefetch amount, > 0 required + * @param delayError delay the error delivery after the normal items? + * @throws IllegalArgumentException if prefetch <= 0 + */ + @SuppressWarnings("unchecked") + public OnSubscribePublishMulticast(int prefetch, boolean delayError) { + if (prefetch <= 0) { + throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); + } + this.prefetch = prefetch; + this.delayError = delayError; + if (UnsafeAccess.isUnsafeAvailable()) { + this.queue = new SpscArrayQueue(prefetch); + } else { + this.queue = new SpscAtomicArrayQueue(prefetch); + } + this.subscribers = (PublishProducer[]) EMPTY; + this.parent = new ParentSubscriber(this); + } + + @Override + public void call(Subscriber t) { + PublishProducer pp = new PublishProducer(t, this); + t.add(pp); + t.setProducer(pp); + + if (add(pp)) { + if (pp.isUnsubscribed()) { + remove(pp); + } else { + drain(); + } + } else { + Throwable e = error; + if (e != null) { + t.onError(e); + } else { + t.onCompleted(); + } + } + } + + @Override + public void onNext(T t) { + if (!queue.offer(t)) { + parent.unsubscribe(); + + error = new MissingBackpressureException("Queue full?!"); + done = true; + } + drain(); + } + + @Override + public void onError(Throwable e) { + error = e; + done = true; + drain(); + } + + @Override + public void onCompleted() { + done = true; + drain(); + } + + /** + * Sets the main producer and issues the prefetch amount. + * @param p the producer to set + */ + void setProducer(Producer p) { + this.producer = p; + p.request(prefetch); + } + + /** + * The serialization loop that determines the minimum request of + * all subscribers and tries to emit as many items from the queue if + * they are available. + * + *

    The execution of the drain-loop is guaranteed to be thread-safe. + */ + void drain() { + if (getAndIncrement() != 0) { + return; + } + + final Queue q = queue; + + int missed = 0; + + for (;;) { + + long r = Long.MAX_VALUE; + PublishProducer[] a = subscribers; + int n = a.length; + + for (PublishProducer inner : a) { + r = Math.min(r, inner.get()); + } + + if (n != 0) { + long e = 0L; + + while (e != r) { + boolean d = done; + + T v = q.poll(); + + boolean empty = v == null; + + if (checkTerminated(d, empty)) { + return; + } + + if (empty) { + break; + } + + for (PublishProducer inner : a) { + inner.actual.onNext(v); + } + + e++; + } + + if (e == r) { + if (checkTerminated(done, q.isEmpty())) { + return; + } + } + + if (e != 0L) { + Producer p = producer; + if (p != null) { + p.request(e); + } + for (PublishProducer inner : a) { + BackpressureUtils.produced(inner, e); + } + + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + /** + * Given the current source state, terminates all child subscribers. + * @param d the source-done indicator + * @param empty the queue-emptiness indicator + * @return true if the class reached its terminal state + */ + boolean checkTerminated(boolean d, boolean empty) { + if (d) { + if (delayError) { + if (empty) { + PublishProducer[] a = terminate(); + Throwable ex = error; + if (ex != null) { + for (PublishProducer inner : a) { + inner.actual.onError(ex); + } + } else { + for (PublishProducer inner : a) { + inner.actual.onCompleted(); + } + } + return true; + } + } else { + Throwable ex = error; + if (ex != null) { + queue.clear(); + PublishProducer[] a = terminate(); + for (PublishProducer inner : a) { + inner.actual.onError(ex); + } + return true; + } else + if (empty) { + PublishProducer[] a = terminate(); + for (PublishProducer inner : a) { + inner.actual.onCompleted(); + } + return true; + } + } + } + return false; + } + + /** + * Atomically swaps in the terminated state. + * @return the last set of subscribers before the state change or an empty array + */ + @SuppressWarnings("unchecked") + PublishProducer[] terminate() { + PublishProducer[] a = subscribers; + if (a != TERMINATED) { + synchronized (this) { + a = subscribers; + if (a != TERMINATED) { + subscribers = (PublishProducer[]) TERMINATED; + } + } + } + return a; + } + + /** + * Atomically adds the given wrapper of a child Subscriber to the subscribers array. + * @param inner the wrapper + * @return true if successful, false if the terminal state has been reached in the meantime + */ + boolean add(PublishProducer inner) { + PublishProducer[] a = subscribers; + if (a == TERMINATED) { + return false; + } + synchronized (this) { + a = subscribers; + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + PublishProducer[] b = new PublishProducer[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + subscribers = b; + return true; + } + } + + /** + * Atomically removes the given wrapper, if present, from the subscribers array. + * @param inner the wrapper to remove + */ + @SuppressWarnings("unchecked") + void remove(PublishProducer inner) { + PublishProducer[] a = subscribers; + if (a == TERMINATED || a == EMPTY) { + return; + } + synchronized (this) { + a = subscribers; + if (a == TERMINATED || a == EMPTY) { + return; + } + + int j = -1; + int n = a.length; + + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + PublishProducer[] b; + if (n == 1) { + b = (PublishProducer[])EMPTY; + } else { + b = new PublishProducer[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + subscribers = b; + } + } + + /** + * The subscriber that must be used for subscribing to the upstream source. + * @param the input value type; + */ + static final class ParentSubscriber extends Subscriber { + /** The reference to the parent state where the events are forwarded to. */ + final OnSubscribePublishMulticast state; + + public ParentSubscriber(OnSubscribePublishMulticast state) { + super(); + this.state = state; + } + + @Override + public void onNext(T t) { + state.onNext(t); + } + + @Override + public void onError(Throwable e) { + state.onError(e); + } + + @Override + public void onCompleted() { + state.onCompleted(); + } + + @Override + public void setProducer(Producer p) { + state.setProducer(p); + } + } + + /** + * Returns the input subscriber of this class that must be subscribed + * to the upstream source. + * @return the subscriber instance + */ + public Subscriber subscriber() { + return parent; + } + + @Override + public void unsubscribe() { + parent.unsubscribe(); + } + + @Override + public boolean isUnsubscribed() { + return parent.isUnsubscribed(); + } + + /** + * A Producer and Subscription that wraps a child Subscriber and manages + * its backpressure requests along with its unsubscription from the parent + * class. + * + *

    The class extends AtomicLong that holds onto the child's requested amount. + * + * @param the output value type + */ + static final class PublishProducer + extends AtomicLong + implements Producer, Subscription { + /** */ + private static final long serialVersionUID = 960704844171597367L; + + /** The actual subscriber to receive the events. */ + final Subscriber actual; + + /** The parent object to request draining or removal. */ + final OnSubscribePublishMulticast parent; + + /** Makes sure unsubscription happens only once. */ + final AtomicBoolean once; + + public PublishProducer(Subscriber actual, OnSubscribePublishMulticast parent) { + this.actual = actual; + this.parent = parent; + this.once = new AtomicBoolean(); + } + + @Override + public void request(long n) { + if (n < 0) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); + } else + if (n != 0) { + BackpressureUtils.getAndAddRequest(this, n); + parent.drain(); + } + } + + @Override + public boolean isUnsubscribed() { + return once.get(); + } + + @Override + public void unsubscribe() { + if (once.compareAndSet(false, true)) { + parent.remove(this); + } + } + } +} diff --git a/src/main/java/rx/internal/operators/OperatorPublish.java b/src/main/java/rx/internal/operators/OperatorPublish.java index 65cf83dd25..24cb677f16 100644 --- a/src/main/java/rx/internal/operators/OperatorPublish.java +++ b/src/main/java/rx/internal/operators/OperatorPublish.java @@ -117,19 +117,46 @@ public void call(Subscriber child) { public static Observable create(final Observable source, final Func1, ? extends Observable> selector) { + return create(source, selector, false); + } + + public static Observable create(final Observable source, + final Func1, ? extends Observable> selector, final boolean delayError) { return create(new OnSubscribe() { @Override public void call(final Subscriber child) { - ConnectableObservable op = create(source); - - selector.call(op).unsafeSubscribe(child); + final OnSubscribePublishMulticast op = new OnSubscribePublishMulticast(RxRingBuffer.SIZE, delayError); - op.connect(new Action1() { + Subscriber subscriber = new Subscriber() { + @Override + public void onNext(R t) { + child.onNext(t); + } + + @Override + public void onError(Throwable e) { + op.unsubscribe(); + child.onError(e); + } + + @Override + public void onCompleted() { + op.unsubscribe(); + child.onCompleted(); + } + @Override - public void call(Subscription t1) { - child.add(t1); + public void setProducer(Producer p) { + child.setProducer(p); } - }); + }; + + child.add(op); + child.add(subscriber); + + selector.call(Observable.create(op)).unsafeSubscribe(subscriber); + + source.unsafeSubscribe(op.subscriber()); } }); } diff --git a/src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java b/src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java new file mode 100644 index 0000000000..761dead9c5 --- /dev/null +++ b/src/test/java/rx/internal/operators/OperatorPublishFunctionTest.java @@ -0,0 +1,255 @@ +/** + * 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 static org.junit.Assert.fail; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; + +import rx.Observable; +import rx.exceptions.MissingBackpressureException; +import rx.functions.Func1; +import rx.internal.util.RxRingBuffer; +import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; + +public class OperatorPublishFunctionTest { + @Test + public void concatTakeFirstLastCompletes() { + TestSubscriber ts = new TestSubscriber(); + + Observable.range(1, 3).publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return Observable.concat(o.take(5), o.takeLast(5)); + } + }).subscribe(ts); + + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void concatTakeFirstLastBackpressureCompletes() { + TestSubscriber ts = TestSubscriber.create(0L); + + Observable.range(1, 6).publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return Observable.concat(o.take(5), o.takeLast(5)); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(5); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(5); + + ts.assertValues(1, 2, 3, 4, 5, 6); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void canBeCancelled() { + TestSubscriber ts = TestSubscriber.create(); + + PublishSubject ps = PublishSubject.create(); + + ps.publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return Observable.concat(o.take(5), o.takeLast(5)); + } + }).subscribe(ts); + + ps.onNext(1); + ps.onNext(2); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.unsubscribe(); + + Assert.assertFalse("Source has subscribers?", ps.hasObservers()); + } + + @Test + public void invalidPrefetch() { + try { + new OnSubscribePublishMulticast(-99, false); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("prefetch > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void takeCompletes() { + TestSubscriber ts = TestSubscriber.create(); + + PublishSubject ps = PublishSubject.create(); + + ps.publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return o.take(1); + } + }).subscribe(ts); + + ps.onNext(1); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertCompleted(); + + Assert.assertFalse("Source has subscribers?", ps.hasObservers()); + + } + + @Test + public void oneStartOnly() { + + final AtomicInteger startCount = new AtomicInteger(); + + TestSubscriber ts = new TestSubscriber() { + @Override + public void onStart() { + startCount.incrementAndGet(); + } + }; + + PublishSubject ps = PublishSubject.create(); + + ps.publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return o.take(1); + } + }).subscribe(ts); + + Assert.assertEquals(1, startCount.get()); + } + + @Test + public void takeCompletesUnsafe() { + TestSubscriber ts = TestSubscriber.create(); + + PublishSubject ps = PublishSubject.create(); + + ps.publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return o.take(1); + } + }).unsafeSubscribe(ts); + + ps.onNext(1); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertCompleted(); + + Assert.assertFalse("Source has subscribers?", ps.hasObservers()); + } + + @Test + public void directCompletesUnsafe() { + TestSubscriber ts = TestSubscriber.create(); + + PublishSubject ps = PublishSubject.create(); + + ps.publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return o; + } + }).unsafeSubscribe(ts); + + ps.onNext(1); + ps.onCompleted(); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertCompleted(); + + Assert.assertFalse("Source has subscribers?", ps.hasObservers()); + } + + @Test + public void oveflowMissingBackpressureException() { + TestSubscriber ts = TestSubscriber.create(0); + + PublishSubject ps = PublishSubject.create(); + + ps.publish(new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return o; + } + }).unsafeSubscribe(ts); + + for (int i = 0; i < RxRingBuffer.SIZE * 2; i++) { + ps.onNext(i); + } + + ts.assertNoValues(); + ts.assertError(MissingBackpressureException.class); + ts.assertNotCompleted(); + + Assert.assertEquals("Queue full?!", ts.getOnErrorEvents().get(0).getMessage()); + Assert.assertFalse("Source has subscribers?", ps.hasObservers()); + } + + @Test + public void oveflowMissingBackpressureExceptionDelayed() { + TestSubscriber ts = TestSubscriber.create(0); + + PublishSubject ps = PublishSubject.create(); + + OperatorPublish.create(ps, new Func1, Observable>() { + @Override + public Observable call(Observable o) { + return o; + } + }, true).unsafeSubscribe(ts); + + for (int i = 0; i < RxRingBuffer.SIZE * 2; i++) { + ps.onNext(i); + } + + ts.requestMore(RxRingBuffer.SIZE); + + ts.assertValueCount(RxRingBuffer.SIZE); + ts.assertError(MissingBackpressureException.class); + ts.assertNotCompleted(); + + Assert.assertEquals("Queue full?!", ts.getOnErrorEvents().get(0).getMessage()); + Assert.assertFalse("Source has subscribers?", ps.hasObservers()); + } +} From 02006ebef3117b264b206f6595c9f8255de558b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 11 Feb 2016 19:59:48 +0100 Subject: [PATCH 522/857] Release 1.1.1 changes.md --- CHANGES.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 2187b97999..09d87dda52 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,91 @@ # RxJava Releases # +### Version 1.1.1 - February 11, 2016 ([Maven](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.1.1%7C)) + +#### The new `Completable` class + + - [Pull 3444](https://github.com/ReactiveX/RxJava/pull/3444) Completable class to support valueless event composition + tests + +##### What is this Completable class? + +We can think of a `Completable` object as a stripped version of `Observable` where only the terminal events, `onError` and `onCompleted` are ever emitted; they may look like an `Observable.empty()` typified in a concrete class but unlike `empty()`, `Completable` is an active class. `Completable` mandates side effects when subscribed to and it is its main purpose indeed. `Completable` contains some deferred computation with side effects and only notifies about the success or failure of such computation. + +Similar to `Single`, the `Completable` behavior can be emulated with `Observable` to some extent, but many API designers think codifying the valuelessness in a separate type is more expressive than messing with wildcards (and usually type-variance problems) in an `Observable` chain. + +`Completable` doesn't stream a single value, therefore, it doesn't need backpressure, simplifying the internal structure from one perspective, however, optimizing these internals requires more lock-free atomics knowledge in some respect. + + +##### Hello World! + +Let's see how one can build a (side-effecting) Hello World `Completable`: + +```java +Completable.fromAction(() -> System.out.println("Hello World!")) +.subscribe(); +``` + +Quite straightforward. We have a set of `fromXXX` method which can take many sources: `Action`, `Callable`, `Single` and even `Observable` (stripping any values generated by the latter 3 of course). On the receiving end, we have the usual subscribe capabilities: empty, lambdas for the terminal events, a `rx.Subscriber` and a `rx.Completable.CompletableSubscriber`, the main intended receiver for `Completable`s. + +##### Further reading + + - [The new Completable API - part 1](http://akarnokd.blogspot.hu/2015/12/the-new-completable-api-part-1.html) + - [The new Completable API - part 2](http://akarnokd.blogspot.hu/2015/12/the-new-completable-api-part-2-final.html) + +#### API enhancements + + - [Pull 3434](https://github.com/ReactiveX/RxJava/pull/3434) Add Single.doAfterTerminate() + - [Pull 3447](https://github.com/ReactiveX/RxJava/pull/3447) operator DelaySubscription with plain Observable + - [Pull 3498](https://github.com/ReactiveX/RxJava/pull/3498) Rename cache(int) to cacheWithCapacityHint(int) + - [Pull 3539](https://github.com/ReactiveX/RxJava/pull/3539) Add Single.zip() for Iterable of Singles + - [Pull 3562](https://github.com/ReactiveX/RxJava/pull/3562) Add Single.doOnUnsubscribe() + - [Pull 3566](https://github.com/ReactiveX/RxJava/pull/3566) Deprecate Observable.finallyDo() and add Observable.doAfterTerminate() instead + - [Pull 3567](https://github.com/ReactiveX/RxJava/pull/3567) Implemented Observable#toCompletable + - [Pull 3570](https://github.com/ReactiveX/RxJava/pull/3570) Implemented Completable#andThen(Observable) + - [Pull 3627](https://github.com/ReactiveX/RxJava/pull/3627) Added MergeDelay operators for Iterable of Observables + - [Pull 3655](https://github.com/ReactiveX/RxJava/pull/3655) Add Single.onErrorResumeNext(Single) + - [Pull 3661](https://github.com/ReactiveX/RxJava/pull/3661) CombineLatest now supports any number of sources + - [Pull 3682](https://github.com/ReactiveX/RxJava/pull/3682) fix observeOn resource handling, add delayError capability + - [Pull 3686](https://github.com/ReactiveX/RxJava/pull/3686) Added retry and retryWhen support for Single + +#### API deprecations + + - `cache(int)` via #3498, replaced by `cacheWithCapacityHint(int)` + - `finallyDo(Action0)` via #3566, replaced by `doAfterTerminate(Action0)` + +### Performance improvements + + - [Pull 3477](https://github.com/ReactiveX/RxJava/pull/3477) add a source OnSubscribe which works from an array directly + - [Pull 3579](https://github.com/ReactiveX/RxJava/pull/3579) No more need to convert Singles to Observables for Single.zip() + - [Pull 3587](https://github.com/ReactiveX/RxJava/pull/3587) Remove the need for javac to generate synthetic methods + - [Pull 3614](https://github.com/ReactiveX/RxJava/pull/3614) just() now supports backpressure + - [Pull 3642](https://github.com/ReactiveX/RxJava/pull/3642) Optimizate single just + - [Pull 3589](https://github.com/ReactiveX/RxJava/pull/3589) concat reduce overhead when streaming a source + +#### Bugfixes + + - [Pull 3428](https://github.com/ReactiveX/RxJava/pull/3428) GroupBy backpressure fix + - [Pull 3454](https://github.com/ReactiveX/RxJava/pull/3454) fix: bounded replay() not requesting enough for latecommers + - [Pull 3467](https://github.com/ReactiveX/RxJava/pull/3467) compensate for drastic clock drifts when scheduling periodic tasks + - [Pull 3555](https://github.com/ReactiveX/RxJava/pull/3555) fix toMap and toMultimap not handling exceptions of the callbacks + - [Pull 3585](https://github.com/ReactiveX/RxJava/pull/3585) fix Completable.using not disposing the resource if the factory crashes during the subscription phase + - [Pull 3588](https://github.com/ReactiveX/RxJava/pull/3588) Fix the initialization order in GenericScheduledExecutorService + - [Pull 3620](https://github.com/ReactiveX/RxJava/pull/3620) Fix NPE in CompositeException when nested throws on initCause + - [Pull 3630](https://github.com/ReactiveX/RxJava/pull/3630) ConcatMapEager allow nulls from inner Observables. + - [Pull 3637](https://github.com/ReactiveX/RxJava/pull/3637) handle predicate exceptions properly in skipWhile + - [Pull 3638](https://github.com/ReactiveX/RxJava/pull/3638) fix error handling in OperatorDistinctUntilChanged + - [Pull 3639](https://github.com/ReactiveX/RxJava/pull/3639) fix error handling in onBackpressureBuffer + - [Pull 3640](https://github.com/ReactiveX/RxJava/pull/3640) fix error handling in onBackpressureDrop + - [Pull 3644](https://github.com/ReactiveX/RxJava/pull/3644) fix SyncOnSubscribe not signalling onError if the generator crashes + - [Pull 3645](https://github.com/ReactiveX/RxJava/pull/3645) fix Amb sharing the choice among all subscribers + - [Pull 3653](https://github.com/ReactiveX/RxJava/pull/3653) fix sample(Observable) not requesting Long.MAX_VALUE + - [Pull 3658](https://github.com/ReactiveX/RxJava/pull/3658) fix unsubscription and producer issues in sample(other) + - [Pull 3662](https://github.com/ReactiveX/RxJava/pull/3662) fix doOnRequest premature requesting + - [Pull 3677](https://github.com/ReactiveX/RxJava/pull/3677) negative argument check for skip's count and merge's maxConcurrent + - [Pull 3681](https://github.com/ReactiveX/RxJava/pull/3681) change publish(Func1) to use a dedicated subject-like dispatcher + - [Pull 3688](https://github.com/ReactiveX/RxJava/pull/3688) Fix zip() - observer array becoming visible too early and causing NPE + - [Pull 3689](https://github.com/ReactiveX/RxJava/pull/3689) unified onErrorX and onExceptionResumeNext and fixed backpressure + + ### Version 1.1.0 – December 2 2015 ([Maven Central](http://search.maven.org/#artifactdetails%7Cio.reactivex%7Crxjava%7C1.1.0%7C)) ### * [Pull 3550] (https://github.com/ReactiveX/RxJava/pull/3550) Public API changes for 1.1.0 release From e9533824ba932500e3cfc40aa0c6fd2cfc3036ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Thu, 11 Feb 2016 23:54:47 +0100 Subject: [PATCH 523/857] 1.x: fix ScalarSynchronousObservable expects EventLoopsScheduler from Schedulers.computation() --- .../util/ScalarSynchronousObservable.java | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java index f4c8c3cd2e..ecb5c18d98 100644 --- a/src/main/java/rx/internal/util/ScalarSynchronousObservable.java +++ b/src/main/java/rx/internal/util/ScalarSynchronousObservable.java @@ -23,7 +23,6 @@ import rx.internal.producers.SingleProducer; import rx.internal.schedulers.EventLoopsScheduler; import rx.observers.Subscribers; -import rx.schedulers.Schedulers; /** * An Observable that emits a single constant scalar value to Subscribers. @@ -34,19 +33,6 @@ * @param the value type */ public final class ScalarSynchronousObservable extends Observable { - - /** - * We expect the Schedulers.computation() to return an EventLoopsScheduler all the time. - */ - static final Func1 COMPUTATION_ONSCHEDULE = new Func1() { - final EventLoopsScheduler els = (EventLoopsScheduler)Schedulers.computation(); - - @Override - public Subscription call(Action0 t) { - return els.scheduleDirect(t); - } - }; - /** * Indicates that the Producer used by this Observable should be fully * threadsafe. It is possible, but unlikely that multiple concurrent @@ -115,7 +101,13 @@ public T get() { public Observable scalarScheduleOn(final Scheduler scheduler) { final Func1 onSchedule; if (scheduler instanceof EventLoopsScheduler) { - onSchedule = COMPUTATION_ONSCHEDULE; + final EventLoopsScheduler els = (EventLoopsScheduler) scheduler; + onSchedule = new Func1() { + @Override + public Subscription call(Action0 a) { + return els.scheduleDirect(a); + } + }; } else { onSchedule = new Func1() { @Override From 0644e88606e724ef6887bfe596614f655f29b3e7 Mon Sep 17 00:00:00 2001 From: Duncan Irvine Date: Fri, 12 Feb 2016 14:39:13 +1100 Subject: [PATCH 524/857] [#3698] Failing Test Case --- .../operators/OperatorGroupByTest.java | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index 3474e4c905..c32e09da9a 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -1581,4 +1581,224 @@ public void call(GroupedObservable g) { ts2.assertNotCompleted(); } + @Test + public void testGroupedObservableCollection() { + + final TestSubscriber> inner1 = new TestSubscriber>(); + final TestSubscriber> inner2 = new TestSubscriber>(); + + TestSubscriber>>> outer = new TestSubscriber>>>(new Subscriber>>>() { + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(List>> o) { + o.get(0).subscribe(inner1); + o.get(1).subscribe(inner2); + } + }); + + + + + Observable.range(0, 10) + .groupBy(new Func1() { + @Override + public Boolean call(Integer pair) { + return pair % 2 == 1; + } + }) + .map(new Func1, Observable>>() { + @Override + public Observable> call(GroupedObservable oddOrEven) { + return oddOrEven.toList(); + } + }) + .toList() + .subscribe(outer); + + inner1.assertNoErrors(); + inner1.assertCompleted(); + inner2.assertNoErrors(); + inner2.assertCompleted(); + + inner1.assertReceivedOnNext(Arrays.asList(Arrays.asList(0,2,4,6,8))); + inner2.assertReceivedOnNext(Arrays.asList(Arrays.asList(1,3,5,7,9))); + + outer.assertNoErrors(); + outer.assertCompleted(); + outer.assertValueCount(1); + + } + + @Test + public void testCollectedGroups() { + + final TestSubscriber> inner1 = new TestSubscriber>(); + final TestSubscriber> inner2 = new TestSubscriber>(); + + final List>> inners = Arrays.asList(inner1, inner2); + + TestSubscriber>> outer = new TestSubscriber>>(new Subscriber>>() { + int toInner; + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Observable> o) { + o.subscribe(inners.get(toInner++)); + } + }); + + + + + Observable.range(0, 10) + .groupBy(new Func1() { + @Override + public Boolean call(Integer pair) { + return pair % 2 == 1; + } + }) + .map(new Func1, Observable>>() { + @Override + public Observable> call(GroupedObservable booleanIntegerGroupedObservable) { + return booleanIntegerGroupedObservable.toList(); + } + }) + .subscribe(outer); + + inner1.assertNoErrors(); + inner1.assertCompleted(); + + inner1.assertReceivedOnNext(Arrays.asList(Arrays.asList(0,2,4,6,8))); + inner2.assertReceivedOnNext(Arrays.asList(Arrays.asList(1,3,5,7,9))); + + outer.assertNoErrors(); + outer.assertCompleted(); + outer.assertValueCount(2); + + } + + @Test + public void testMappedCollectedGroups() { + // This is a little contrived. + final TestSubscriber inner1 = new TestSubscriber(); + final TestSubscriber inner2 = new TestSubscriber(); + + TestSubscriber>> outer = new TestSubscriber>>(new Subscriber>>() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Map> integerObservableMap) { + integerObservableMap.get(0).subscribe(inner1); + integerObservableMap.get(1).subscribe(inner2); + } + }); + + Observable>> mapObservable = Observable.range(0, 10) + .groupBy(new Func1() { + @Override + public Integer call(Integer pair) { + return pair % 2; + } + }) + .toMap(new Func1, Integer>() { + @Override + public Integer call(GroupedObservable group) { + return group.getKey(); + } + }, + new Func1, Observable>() { + @Override + public Observable call(GroupedObservable integerGroup) { + return integerGroup.map( + new Func1() { + @Override + public Integer call(Integer integer) { + return integer * 10; + } + }); + } + } + ); + + mapObservable.subscribe(outer); + + inner1.assertNoErrors(); + inner1.assertCompleted(); + + inner1.assertReceivedOnNext(Arrays.asList(0,20,40,60,80)); + inner2.assertReceivedOnNext(Arrays.asList(10,30,50,70,90)); + + outer.assertNoErrors(); + outer.assertCompleted(); + outer.assertValueCount(1); + + } + + @Test + public void testSkippedGroup() { + + final TestSubscriber inner1 = new TestSubscriber(); + + TestSubscriber> outer = new TestSubscriber>(new Subscriber>() { + + @Override + public void onCompleted() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(GroupedObservable o) { + if (o.getKey() == 1) { + o.subscribe(inner1); + } + } + }); + + + + + Observable.range(0, 10) + .groupBy(new Func1() { + @Override + public Integer call(Integer pair) { + return pair % 2; + } + }) + .subscribe(outer); + + inner1.assertNoErrors(); + inner1.assertCompleted(); + + inner1.assertReceivedOnNext(Arrays.asList(1,3,5,7,9)); + + outer.assertNoErrors(); + outer.assertCompleted(); + outer.assertValueCount(2); + + } } From 82f5da59fedbd3cadace2f16787c2f81d3910e12 Mon Sep 17 00:00:00 2001 From: Zach Klippenstein Date: Fri, 12 Feb 2016 11:25:58 -0800 Subject: [PATCH 525/857] 1.x: alias Observable.doOnCompleted to match Completable and 2x Closes #3700. --- src/main/java/rx/Completable.java | 15 +++++++++++++-- src/test/java/rx/CompletableTest.java | 20 ++++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index 971763cc46..2b0940afe1 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -1222,9 +1222,20 @@ public void onSubscribe(Subscription d) { * @param onComplete the callback to call when this emits an onComplete event * @return the new Completable instance * @throws NullPointerException if onComplete is null + * @deprecated Use {@link #doOnCompleted(Action0)} instead. */ - public final Completable doOnComplete(Action0 onComplete) { - return doOnLifecycle(Actions.empty(), Actions.empty(), onComplete, Actions.empty(), Actions.empty()); + @Deprecated public final Completable doOnComplete(Action0 onComplete) { + return doOnCompleted(onComplete); + } + + /** + * Returns a Completable which calls the given onCompleted callback if this Completable completes. + * @param onCompleted the callback to call when this emits an onComplete event + * @return the new Completable instance + * @throws NullPointerException if onComplete is null + */ + public final Completable doOnCompleted(Action0 onCompleted) { + return doOnLifecycle(Actions.empty(), Actions.empty(), onCompleted, Actions.empty(), Actions.empty()); } /** diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index 6f60c57347..6261d18f93 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -1673,10 +1673,10 @@ public void onCompleted() { } @Test(timeout = 1000) - public void doOnCompleteNormal() { + public void doOnCompletedNormal() { final AtomicInteger calls = new AtomicInteger(); - Completable c = normal.completable.doOnComplete(new Action0() { + Completable c = normal.completable.doOnCompleted(new Action0() { @Override public void call() { calls.getAndIncrement(); @@ -1689,10 +1689,10 @@ public void call() { } @Test(timeout = 1000) - public void doOnCompleteError() { + public void doOnCompletedError() { final AtomicInteger calls = new AtomicInteger(); - Completable c = error.completable.doOnComplete(new Action0() { + Completable c = error.completable.doOnCompleted(new Action0() { @Override public void call() { calls.getAndIncrement(); @@ -1710,13 +1710,13 @@ public void call() { } @Test(expected = NullPointerException.class) - public void doOnCompleteNull() { - normal.completable.doOnComplete(null); + public void doOnCompletedNull() { + normal.completable.doOnCompleted(null); } @Test(timeout = 1000, expected = TestException.class) - public void doOnCompleteThrows() { - Completable c = normal.completable.doOnComplete(new Action0() { + public void doOnCompletedThrows() { + Completable c = normal.completable.doOnCompleted(new Action0() { @Override public void call() { throw new TestException(); } }); @@ -2469,7 +2469,7 @@ public void subscribe() throws InterruptedException { Completable c = normal.completable .delay(100, TimeUnit.MILLISECONDS) - .doOnComplete(new Action0() { + .doOnCompleted(new Action0() { @Override public void call() { complete.set(true); @@ -2489,7 +2489,7 @@ public void subscribeDispose() throws InterruptedException { Completable c = normal.completable .delay(200, TimeUnit.MILLISECONDS) - .doOnComplete(new Action0() { + .doOnCompleted(new Action0() { @Override public void call() { complete.set(true); From a6f35a58e2feee9d22a525a11c9c009e6ac6ab40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Fri, 12 Feb 2016 22:33:01 +0100 Subject: [PATCH 526/857] 1.x: fix mapNotification's last item backpressure handling --- .../operators/OperatorMapNotification.java | 286 ++++++++---------- .../OperatorMapNotificationTest.java | 85 ++++++ 2 files changed, 208 insertions(+), 163 deletions(-) diff --git a/src/main/java/rx/internal/operators/OperatorMapNotification.java b/src/main/java/rx/internal/operators/OperatorMapNotification.java index 8abe7b828e..e7a18cc202 100644 --- a/src/main/java/rx/internal/operators/OperatorMapNotification.java +++ b/src/main/java/rx/internal/operators/OperatorMapNotification.java @@ -15,16 +15,12 @@ */ package rx.internal.operators; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.*; import rx.*; import rx.Observable.Operator; -import rx.exceptions.*; +import rx.exceptions.Exceptions; import rx.functions.*; -import rx.internal.producers.ProducerArbiter; -import rx.internal.util.unsafe.*; /** * Applies a function of your choosing to every item emitted by an {@code Observable}, and emits the results of @@ -45,203 +41,167 @@ public OperatorMapNotification(Func1 onNext, Func1 call(final Subscriber o) { - final ProducerArbiter pa = new ProducerArbiter(); - - MapNotificationSubscriber subscriber = new MapNotificationSubscriber(pa, o); - o.add(subscriber); - subscriber.init(); - return subscriber; + public Subscriber call(final Subscriber child) { + final MapNotificationSubscriber parent = new MapNotificationSubscriber(child, onNext, onError, onCompleted); + child.add(parent); + child.setProducer(new Producer() { + @Override + public void request(long n) { + parent.requestInner(n); + } + }); + return parent; } - final class MapNotificationSubscriber extends Subscriber { - private final Subscriber o; - private final ProducerArbiter pa; - final SingleEmitter emitter; - - MapNotificationSubscriber(ProducerArbiter pa, Subscriber o) { - this.pa = pa; - this.o = o; - this.emitter = new SingleEmitter(o, pa, this); - } + static final class MapNotificationSubscriber extends Subscriber { - void init() { - o.setProducer(emitter); - } + final Subscriber actual; + + final Func1 onNext; + + final Func1 onError; + + final Func0 onCompleted; + + final AtomicLong requested; - @Override - public void setProducer(Producer producer) { - pa.setProducer(producer); + final AtomicLong missedRequested; + + final AtomicReference producer; + + long produced; + + R value; + + static final long COMPLETED_FLAG = Long.MIN_VALUE; + static final long REQUESTED_MASK = Long.MAX_VALUE; + + public MapNotificationSubscriber(Subscriber actual, Func1 onNext, + Func1 onError, Func0 onCompleted) { + this.actual = actual; + this.onNext = onNext; + this.onError = onError; + this.onCompleted = onCompleted; + this.requested = new AtomicLong(); + this.missedRequested = new AtomicLong(); + this.producer = new AtomicReference(); } @Override - public void onCompleted() { + public void onNext(T t) { try { - emitter.offerAndComplete(onCompleted.call()); - } catch (Throwable e) { - Exceptions.throwOrReport(e, o); + produced++; + actual.onNext(onNext.call(t)); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, actual, t); } } - + @Override public void onError(Throwable e) { + accountProduced(); try { - emitter.offerAndComplete(onError.call(e)); - } catch (Throwable e2) { - Exceptions.throwOrReport(e2, o); + value = onError.call(e); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, actual, e); } + tryEmit(); } - + @Override - public void onNext(T t) { + public void onCompleted() { + accountProduced(); try { - emitter.offer(onNext.call(t)); - } catch (Throwable e) { - Exceptions.throwOrReport(e, o, t); + value = onCompleted.call(); + } catch (Throwable ex) { + Exceptions.throwOrReport(ex, actual); } + tryEmit(); } - } - static final class SingleEmitter extends AtomicLong implements Producer, Subscription { - /** */ - private static final long serialVersionUID = -249869671366010660L; - final NotificationLite nl; - final Subscriber child; - final Producer producer; - final Subscription cancel; - final Queue queue; - volatile boolean complete; - /** Guarded by this. */ - boolean emitting; - /** Guarded by this. */ - boolean missed; - public SingleEmitter(Subscriber child, Producer producer, Subscription cancel) { - this.child = child; - this.producer = producer; - this.cancel = cancel; - this.queue = UnsafeAccess.isUnsafeAvailable() - ? new SpscArrayQueue(2) - : new ConcurrentLinkedQueue(); - - this.nl = NotificationLite.instance(); + void accountProduced() { + long p = produced; + if (p != 0L && producer.get() != null) { + BackpressureUtils.produced(requested, p); + } } + @Override - public void request(long n) { - for (;;) { - long r = get(); - if (r < 0) { - return; - } - long u = r + n; - if (u < 0) { - u = Long.MAX_VALUE; - } - if (compareAndSet(r, u)) { - producer.request(n); - drain(); - return; + public void setProducer(Producer p) { + if (producer.compareAndSet(null, p)) { + long r = missedRequested.getAndSet(0L); + if (r != 0L) { + p.request(r); } + } else { + throw new IllegalStateException("Producer already set!"); } } - void produced(long n) { + void tryEmit() { for (;;) { - long r = get(); - if (r < 0) { - return; - } - long u = r - n; - if (u < 0) { - throw new IllegalStateException("More produced (" + n + ") than requested (" + r + ")"); + long r = requested.get(); + if ((r & COMPLETED_FLAG) != 0) { + break; } - if (compareAndSet(r, u)) { + if (requested.compareAndSet(r, r | COMPLETED_FLAG)) { + if (r != 0 || producer.get() == null) { + if (!actual.isUnsubscribed()) { + actual.onNext(value); + } + if (!actual.isUnsubscribed()) { + actual.onCompleted(); + } + } return; } } } - public void offer(T value) { - if (!queue.offer(value)) { - child.onError(new MissingBackpressureException()); - unsubscribe(); - } else { - drain(); + void requestInner(long n) { + if (n < 0L) { + throw new IllegalArgumentException("n >= 0 required but it was " + n); } - } - public void offerAndComplete(T value) { - if (!this.queue.offer(value)) { - child.onError(new MissingBackpressureException()); - unsubscribe(); - } else { - this.complete = true; - drain(); - } - } - - void drain() { - synchronized (this) { - if (emitting) { - missed = true; - return; - } - emitting = true; - missed = false; + if (n == 0L) { + return; } - boolean skipFinal = false; - try { - for (;;) { - - long r = get(); - boolean c = complete; - boolean empty = queue.isEmpty(); - - if (c && empty) { - child.onCompleted(); - skipFinal = true; - return; - } else - if (r > 0) { - Object v = queue.poll(); - if (v != null) { - child.onNext(nl.getValue(v)); - produced(1); - } else - if (c) { - child.onCompleted(); - skipFinal = true; - return; - } - } - - synchronized (this) { - if (!missed) { - skipFinal = true; - emitting = false; - return; + for (;;) { + long r = requested.get(); + + if ((r & COMPLETED_FLAG) != 0L) { + long v = r & REQUESTED_MASK; + long u = BackpressureUtils.addCap(v, n) | COMPLETED_FLAG; + if (requested.compareAndSet(r, u)) { + if (v == 0L) { + if (!actual.isUnsubscribed()) { + actual.onNext(value); + } + if (!actual.isUnsubscribed()) { + actual.onCompleted(); + } } - missed = false; + return; } - } - } finally { - if (!skipFinal) { - synchronized (this) { - emitting = false; + } else { + long u = BackpressureUtils.addCap(r, n); + if (requested.compareAndSet(r, u)) { + break; } } } - } - - @Override - public boolean isUnsubscribed() { - return get() < 0; - } - @Override - public void unsubscribe() { - long r = get(); - if (r != Long.MIN_VALUE) { - r = getAndSet(Long.MIN_VALUE); - if (r != Long.MIN_VALUE) { - cancel.unsubscribe(); + + AtomicReference localProducer = producer; + Producer actualProducer = localProducer.get(); + if (actualProducer != null) { + actualProducer.request(n); + } else { + BackpressureUtils.getAndAddRequest(missedRequested, n); + actualProducer = localProducer.get(); + if (actualProducer != null) { + long r = missedRequested.getAndSet(0L); + if (r != 0L) { + actualProducer.request(r); + } } } } diff --git a/src/test/java/rx/internal/operators/OperatorMapNotificationTest.java b/src/test/java/rx/internal/operators/OperatorMapNotificationTest.java index 2f1e603337..3e94a20b8b 100644 --- a/src/test/java/rx/internal/operators/OperatorMapNotificationTest.java +++ b/src/test/java/rx/internal/operators/OperatorMapNotificationTest.java @@ -21,6 +21,7 @@ import rx.Observable; import rx.functions.*; import rx.observers.TestSubscriber; +import rx.subjects.PublishSubject; public class OperatorMapNotificationTest { @Test @@ -52,4 +53,88 @@ public Observable call() { ts.assertNotCompleted(); ts.assertValue(2); } + + @Test + public void backpressure() { + TestSubscriber ts = TestSubscriber.create(0L); + + Observable.range(1, 3).lift(new OperatorMapNotification( + new Func1() { + @Override + public Integer call(Integer item) { + return item + 1; + } + }, + new Func1() { + @Override + public Integer call(Throwable e) { + return 0; + } + }, + new Func0() { + @Override + public Integer call() { + return 5; + } + } + )).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(3); + + ts.assertValues(2, 3, 4); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ts.requestMore(1); + + ts.assertValues(2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertCompleted(); + } + + @Test + public void noBackpressure() { + TestSubscriber ts = TestSubscriber.create(0L); + + PublishSubject ps = PublishSubject.create(); + + ps.lift(new OperatorMapNotification( + new Func1() { + @Override + public Integer call(Integer item) { + return item + 1; + } + }, + new Func1() { + @Override + public Integer call(Throwable e) { + return 0; + } + }, + new Func0() { + @Override + public Integer call() { + return 5; + } + } + )).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotCompleted(); + + ps.onNext(1); + ps.onNext(2); + ps.onNext(3); + ps.onCompleted(); + + ts.assertValues(2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertCompleted(); + } + } From 271e1b835b223dc354e9de2598111a1846f63e9e Mon Sep 17 00:00:00 2001 From: Duncan Irvine Date: Sat, 13 Feb 2016 13:08:31 +1100 Subject: [PATCH 527/857] [#3698] Correct indentation --- .../operators/OperatorGroupByTest.java | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/test/java/rx/internal/operators/OperatorGroupByTest.java b/src/test/java/rx/internal/operators/OperatorGroupByTest.java index c32e09da9a..6ec0aee15f 100644 --- a/src/test/java/rx/internal/operators/OperatorGroupByTest.java +++ b/src/test/java/rx/internal/operators/OperatorGroupByTest.java @@ -1715,40 +1715,41 @@ public void onNext(Map> integerObservableMap) { } }); - Observable>> mapObservable = Observable.range(0, 10) - .groupBy(new Func1() { - @Override - public Integer call(Integer pair) { - return pair % 2; - } - }) - .toMap(new Func1, Integer>() { - @Override - public Integer call(GroupedObservable group) { - return group.getKey(); - } - }, - new Func1, Observable>() { - @Override - public Observable call(GroupedObservable integerGroup) { - return integerGroup.map( - new Func1() { - @Override - public Integer call(Integer integer) { - return integer * 10; - } - }); - } - } - ); + Observable>> mapObservable = + Observable.range(0, 10) + .groupBy(new Func1() { + @Override + public Integer call(Integer pair) { + return pair % 2; + } + }) + .toMap(new Func1, Integer>() { + @Override + public Integer call(GroupedObservable group) { + return group.getKey(); + } + }, + new Func1, Observable>() { + @Override + public Observable call(GroupedObservable integerGroup) { + return integerGroup.map( + new Func1() { + @Override + public Integer call(Integer integer) { + return integer * 10; + } + }); + } + } + ); mapObservable.subscribe(outer); inner1.assertNoErrors(); inner1.assertCompleted(); - inner1.assertReceivedOnNext(Arrays.asList(0,20,40,60,80)); - inner2.assertReceivedOnNext(Arrays.asList(10,30,50,70,90)); + inner1.assertReceivedOnNext(Arrays.asList(0, 20, 40, 60, 80)); + inner2.assertReceivedOnNext(Arrays.asList(10, 30, 50, 70, 90)); outer.assertNoErrors(); outer.assertCompleted(); From b9a57f0a8b57b5e9401efcf8ee1accf63abad465 Mon Sep 17 00:00:00 2001 From: Shixiong Zhu Date: Sat, 13 Feb 2016 23:52:23 -0800 Subject: [PATCH 528/857] Make the javadoc task generate correct docs --- build.gradle | 13 ++ gradle/stylesheet.css | 474 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 487 insertions(+) create mode 100644 gradle/stylesheet.css diff --git a/build.gradle b/build.gradle index 3789c7cb5c..f465376da3 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,19 @@ dependencies { javadoc { exclude "**/rx/internal/**" + exclude "**/test/**" + exclude "**/perf/**" + options { + windowTitle = "RxJava Javadoc ${project.version}" + } + // Clear the following options to make the docs consitent with the old format + options.addStringOption('top').value = '' + options.addStringOption('doctitle').value = '' + options.addStringOption('header').value = '' + if (JavaVersion.current().isJava7()) { + // "./gradle/stylesheet.css" only supports Java 7 + options.addStringOption('stylesheetfile', rootProject.file('./gradle/stylesheet.css').toString()) + } } // support for snapshot/final releases with the various branches RxJava uses diff --git a/gradle/stylesheet.css b/gradle/stylesheet.css new file mode 100644 index 0000000000..0aeaa97fe0 --- /dev/null +++ b/gradle/stylesheet.css @@ -0,0 +1,474 @@ +/* Javadoc style sheet */ +/* +Overall document style +*/ +body { + background-color:#ffffff; + color:#353833; + font-family:Arial, Helvetica, sans-serif; + font-size:76%; + margin:0; +} +a:link, a:visited { + text-decoration:none; + color:#4c6b87; +} +a:hover, a:focus { + text-decoration:none; + color:#bb7a2a; +} +a:active { + text-decoration:none; + color:#4c6b87; +} +a[name] { + color:#353833; +} +a[name]:hover { + text-decoration:none; + color:#353833; +} +pre { + font-size:1.3em; +} +h1 { + font-size:1.8em; +} +h2 { + font-size:1.5em; +} +h3 { + font-size:1.4em; +} +h4 { + font-size:1.3em; +} +h5 { + font-size:1.2em; +} +h6 { + font-size:1.1em; +} +ul { + list-style-type:disc; +} +code, tt { + font-size:1.2em; +} +dt code { + font-size:1.2em; +} +table tr td dt code { + font-size:1.2em; + vertical-align:top; +} +sup { + font-size:.6em; +} +/* +Document title and Copyright styles +*/ +.clear { + clear:both; + height:0px; + overflow:hidden; +} +.aboutLanguage { + float:right; + padding:0px 21px; + font-size:.8em; + z-index:200; + margin-top:-7px; +} +.legalCopy { + margin-left:.5em; +} +.bar a, .bar a:link, .bar a:visited, .bar a:active { + color:#FFFFFF; + text-decoration:none; +} +.bar a:hover, .bar a:focus { + color:#bb7a2a; +} +.tab { + background-color:#0066FF; + background-image:url(resources/titlebar.gif); + background-position:left top; + background-repeat:no-repeat; + color:#ffffff; + padding:8px; + width:5em; + font-weight:bold; +} +/* +Navigation bar styles +*/ +.bar { + background-image:url(resources/background.gif); + background-repeat:repeat-x; + color:#FFFFFF; + padding:.8em .5em .4em .8em; + height:auto;/*height:1.8em;*/ + font-size:1em; + margin:0; +} +.topNav { + background-image:url(resources/background.gif); + background-repeat:repeat-x; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; +} +.bottomNav { + margin-top:10px; + background-image:url(resources/background.gif); + background-repeat:repeat-x; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; +} +.subNav { + background-color:#dee3e9; + border-bottom:1px solid #9eadc0; + float:left; + width:100%; + overflow:hidden; +} +.subNav div { + clear:left; + float:left; + padding:0 0 5px 6px; +} +ul.navList, ul.subNavList { + float:left; + margin:0 25px 0 0; + padding:0; +} +ul.navList li{ + list-style:none; + float:left; + padding:3px 6px; +} +ul.subNavList li{ + list-style:none; + float:left; + font-size:90%; +} +.topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited { + color:#FFFFFF; + text-decoration:none; +} +.topNav a:hover, .bottomNav a:hover { + text-decoration:none; + color:#bb7a2a; +} +.navBarCell1Rev { + background-image:url(resources/tab.gif); + background-color:#a88834; + color:#FFFFFF; + margin: auto 5px; + border:1px solid #c9aa44; +} +/* +Page header and footer styles +*/ +.header, .footer { + clear:both; + margin:0 20px; + padding:5px 0 0 0; +} +.indexHeader { + margin:10px; + position:relative; +} +.indexHeader h1 { + font-size:1.3em; +} +.title { + color:#2c4557; + margin:10px 0; +} +.subTitle { + margin:5px 0 0 0; +} +.header ul { + margin:0 0 25px 0; + padding:0; +} +.footer ul { + margin:20px 0 5px 0; +} +.header ul li, .footer ul li { + list-style:none; + font-size:1.2em; +} +/* +Heading styles +*/ +div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 { + background-color:#dee3e9; + border-top:1px solid #9eadc0; + border-bottom:1px solid #9eadc0; + margin:0 0 6px -8px; + padding:2px 5px; +} +ul.blockList ul.blockList ul.blockList li.blockList h3 { + background-color:#dee3e9; + border-top:1px solid #9eadc0; + border-bottom:1px solid #9eadc0; + margin:0 0 6px -8px; + padding:2px 5px; +} +ul.blockList ul.blockList li.blockList h3 { + padding:0; + margin:15px 0; +} +ul.blockList li.blockList h2 { + padding:0px 0 20px 0; +} +/* +Page layout container styles +*/ +.contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer { + clear:both; + padding:10px 20px; + position:relative; +} +.indexContainer { + margin:10px; + position:relative; + font-size:1.0em; +} +.indexContainer h2 { + font-size:1.1em; + padding:0 0 3px 0; +} +.indexContainer ul { + margin:0; + padding:0; +} +.indexContainer ul li { + list-style:none; +} +.contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt { + font-size:1.1em; + font-weight:bold; + margin:10px 0 0 0; + color:#4E4E4E; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + margin:10px 0 10px 20px; +} +.serializedFormContainer dl.nameValue dt { + margin-left:1px; + font-size:1.1em; + display:inline; + font-weight:bold; +} +.serializedFormContainer dl.nameValue dd { + margin:0 0 0 1px; + font-size:1.1em; + display:inline; +} +/* +List styles +*/ +ul.horizontal li { + display:inline; + font-size:0.9em; +} +ul.inheritance { + margin:0; + padding:0; +} +ul.inheritance li { + display:inline; + list-style:none; +} +ul.inheritance li ul.inheritance { + margin-left:15px; + padding-left:15px; + padding-top:1px; +} +ul.blockList, ul.blockListLast { + margin:10px 0 10px 0; + padding:0; +} +ul.blockList li.blockList, ul.blockListLast li.blockList { + list-style:none; + margin-bottom:25px; +} +ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList { + padding:0px 20px 5px 10px; + border:1px solid #9eadc0; + background-color:#f9f9f9; +} +ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList { + padding:0 0 5px 8px; + background-color:#ffffff; + border:1px solid #9eadc0; + border-top:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { + margin-left:0; + padding-left:0; + padding-bottom:15px; + border:none; + border-bottom:1px solid #9eadc0; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { + list-style:none; + border-bottom:none; + padding-bottom:0; +} +table tr td dl, table tr td dl dt, table tr td dl dd { + margin-top:0; + margin-bottom:1px; +} +/* +Table styles +*/ +.contentContainer table, .classUseContainer table, .constantValuesContainer table { + border-bottom:1px solid #9eadc0; + width:100%; +} +.contentContainer ul li table, .classUseContainer ul li table, .constantValuesContainer ul li table { + width:100%; +} +.contentContainer .description table, .contentContainer .details table { + border-bottom:none; +} +.contentContainer ul li table th.colOne, .contentContainer ul li table th.colFirst, .contentContainer ul li table th.colLast, .classUseContainer ul li table th, .constantValuesContainer ul li table th, .contentContainer ul li table td.colOne, .contentContainer ul li table td.colFirst, .contentContainer ul li table td.colLast, .classUseContainer ul li table td, .constantValuesContainer ul li table td{ + vertical-align:top; + padding-right:20px; +} +.contentContainer ul li table th.colLast, .classUseContainer ul li table th.colLast,.constantValuesContainer ul li table th.colLast, +.contentContainer ul li table td.colLast, .classUseContainer ul li table td.colLast,.constantValuesContainer ul li table td.colLast, +.contentContainer ul li table th.colOne, .classUseContainer ul li table th.colOne, +.contentContainer ul li table td.colOne, .classUseContainer ul li table td.colOne { + padding-right:3px; +} +.overviewSummary caption, .packageSummary caption, .contentContainer ul.blockList li.blockList caption, .summary caption, .classUseContainer caption, .constantValuesContainer caption { + position:relative; + text-align:left; + background-repeat:no-repeat; + color:#FFFFFF; + font-weight:bold; + clear:none; + overflow:hidden; + padding:0px; + margin:0px; +} +caption a:link, caption a:hover, caption a:active, caption a:visited { + color:#FFFFFF; +} +.overviewSummary caption span, .packageSummary caption span, .contentContainer ul.blockList li.blockList caption span, .summary caption span, .classUseContainer caption span, .constantValuesContainer caption span { + white-space:nowrap; + padding-top:8px; + padding-left:8px; + display:block; + float:left; + background-image:url(resources/titlebar.gif); + height:18px; +} +.overviewSummary .tabEnd, .packageSummary .tabEnd, .contentContainer ul.blockList li.blockList .tabEnd, .summary .tabEnd, .classUseContainer .tabEnd, .constantValuesContainer .tabEnd { + width:10px; + background-image:url(resources/titlebar_end.gif); + background-repeat:no-repeat; + background-position:top right; + position:relative; + float:left; +} +ul.blockList ul.blockList li.blockList table { + margin:0 0 12px 0px; + width:100%; +} +.tableSubHeadingColor { + background-color: #EEEEFF; +} +.altColor { + background-color:#eeeeef; +} +.rowColor { + background-color:#ffffff; +} +.overviewSummary td, .packageSummary td, .contentContainer ul.blockList li.blockList td, .summary td, .classUseContainer td, .constantValuesContainer td { + text-align:left; + padding:3px 3px 3px 7px; +} +th.colFirst, th.colLast, th.colOne, .constantValuesContainer th { + background:#dee3e9; + border-top:1px solid #9eadc0; + border-bottom:1px solid #9eadc0; + text-align:left; + padding:3px 3px 3px 7px; +} +td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover { + font-weight:bold; +} +td.colFirst, th.colFirst { + border-left:1px solid #9eadc0; + white-space:nowrap; +} +td.colLast, th.colLast { + border-right:1px solid #9eadc0; +} +td.colOne, th.colOne { + border-right:1px solid #9eadc0; + border-left:1px solid #9eadc0; +} +table.overviewSummary { + padding:0px; + margin-left:0px; +} +table.overviewSummary td.colFirst, table.overviewSummary th.colFirst, +table.overviewSummary td.colOne, table.overviewSummary th.colOne { + width:25%; + vertical-align:middle; +} +table.packageSummary td.colFirst, table.overviewSummary th.colFirst { + width:25%; + vertical-align:middle; +} +/* +Content styles +*/ +.description pre { + margin-top:0; +} +.deprecatedContent { + margin:0; + padding:10px 0; +} +.docSummary { + padding:0; +} +/* +Formatting effect styles +*/ +.sourceLineNo { + color:green; + padding:0 30px 0 0; +} +h1.hidden { + visibility:hidden; + overflow:hidden; + font-size:.9em; +} +.block { + display:block; + margin:3px 0 0 0; +} +.strong { + font-weight:bold; +} From 00433f3960bcce6cdb13059418059982a6905a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Karnok?= Date: Mon, 15 Feb 2016 01:19:18 +0100 Subject: [PATCH 529/857] 1.x: make Completable.subscribe() report isUnsubscribed consistently --- src/main/java/rx/Completable.java | 8 +- src/test/java/rx/CompletableTest.java | 138 ++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/Completable.java b/src/main/java/rx/Completable.java index 2b0940afe1..b71ee03e20 100644 --- a/src/main/java/rx/Completable.java +++ b/src/main/java/rx/Completable.java @@ -1828,12 +1828,13 @@ public final Subscription subscribe() { subscribe(new CompletableSubscriber() { @Override public void onCompleted() { - // nothing to do + mad.unsubscribe(); } @Override public void onError(Throwable e) { ERROR_HANDLER.handleError(e); + mad.unsubscribe(); } @Override @@ -1864,11 +1865,13 @@ public void onCompleted() { } catch (Throwable e) { ERROR_HANDLER.handleError(e); } + mad.unsubscribe(); } @Override public void onError(Throwable e) { ERROR_HANDLER.handleError(e); + mad.unsubscribe(); } @Override @@ -1900,7 +1903,9 @@ public void onCompleted() { onComplete.call(); } catch (Throwable e) { onError(e); + return; } + mad.unsubscribe(); } @Override @@ -1911,6 +1916,7 @@ public void onError(Throwable e) { e = new CompositeException(Arrays.asList(e, ex)); ERROR_HANDLER.handleError(e); } + mad.unsubscribe(); } @Override diff --git a/src/test/java/rx/CompletableTest.java b/src/test/java/rx/CompletableTest.java index 6261d18f93..97c169c4f5 100644 --- a/src/test/java/rx/CompletableTest.java +++ b/src/test/java/rx/CompletableTest.java @@ -3604,4 +3604,142 @@ public Completable call(Integer t) { assertTrue(listEx.get(1).toString(), listEx.get(1) instanceof TestException); } + @Test + public void subscribeReportsUnsubscribed() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + Subscription completableSubscription = completable.subscribe(); + + stringSubject.onCompleted(); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + } + + @Test + public void subscribeReportsUnsubscribedOnError() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + Subscription completableSubscription = completable.subscribe(); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + } + + @Test + public void subscribeActionReportsUnsubscribed() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + Subscription completableSubscription = completable.subscribe(Actions.empty()); + + stringSubject.onCompleted(); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + } + + @Test + public void subscribeActionReportsUnsubscribedAfter() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + final AtomicReference subscriptionRef = new AtomicReference(); + Subscription completableSubscription = completable.subscribe(new Action0() { + @Override + public void call() { + if (subscriptionRef.get().isUnsubscribed()) { + subscriptionRef.set(null); + } + } + }); + subscriptionRef.set(completableSubscription); + + stringSubject.onCompleted(); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + assertNotNull("Unsubscribed before the call to onCompleted", subscriptionRef.get()); + } + + @Test + public void subscribeActionReportsUnsubscribedOnError() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + Subscription completableSubscription = completable.subscribe(Actions.empty()); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + } + + @Test + public void subscribeAction2ReportsUnsubscribed() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + Subscription completableSubscription = completable.subscribe(Actions.empty(), Actions.empty()); + + stringSubject.onCompleted(); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + } + + @Test + public void subscribeAction2ReportsUnsubscribedOnError() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + Subscription completableSubscription = completable.subscribe(Actions.empty(), Actions.empty()); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + } + + @Test + public void subscribeAction2ReportsUnsubscribedAfter() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + final AtomicReference subscriptionRef = new AtomicReference(); + Subscription completableSubscription = completable.subscribe(Actions.empty(), new Action0() { + @Override + public void call() { + if (subscriptionRef.get().isUnsubscribed()) { + subscriptionRef.set(null); + } + } + }); + subscriptionRef.set(completableSubscription); + + stringSubject.onCompleted(); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + assertNotNull("Unsubscribed before the call to onCompleted", subscriptionRef.get()); + } + + @Test + public void subscribeAction2ReportsUnsubscribedOnErrorAfter() { + PublishSubject stringSubject = PublishSubject.create(); + Completable completable = stringSubject.toCompletable(); + + final AtomicReference subscriptionRef = new AtomicReference(); + Subscription completableSubscription = completable.subscribe(new Action1() { + @Override + public void call(Throwable e) { + if (subscriptionRef.get().isUnsubscribed()) { + subscriptionRef.set(null); + } + } + }, Actions.empty()); + subscriptionRef.set(completableSubscription); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isUnsubscribed()); + assertNotNull("Unsubscribed before the call to onError", subscriptionRef.get()); + } + } \ No newline at end of file From e917c77d296109edead50146691b09c91d3ea748 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Mon, 15 Feb 2016 01:37:52 -0800 Subject: [PATCH 530/857] Add takeUntil support in Single --- src/main/java/rx/Single.java | 157 ++++++++++++- src/test/java/rx/SingleTest.java | 370 ++++++++++++++++++++++++++++++- 2 files changed, 519 insertions(+), 8 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 8f0db3e56b..813dc61a0d 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -12,6 +12,9 @@ */ package rx; +import java.util.Collection; +import java.util.concurrent.*; + import rx.Observable.Operator; import rx.annotations.Beta; import rx.annotations.Experimental; @@ -23,15 +26,13 @@ import rx.internal.util.ScalarSynchronousSingle; import rx.internal.util.UtilityFunctions; import rx.observers.SafeSubscriber; +import rx.observers.SerializedSubscriber; import rx.plugins.RxJavaObservableExecutionHook; import rx.plugins.RxJavaPlugins; import rx.schedulers.Schedulers; import rx.singles.BlockingSingle; import rx.subscriptions.Subscriptions; -import java.util.Collection; -import java.util.concurrent.*; - /** * The Single class implements the Reactive Pattern for a single value response. See {@link Observable} for the * implementation of the Reactive Pattern for a stream or vector of values. @@ -1800,6 +1801,156 @@ public void onError(Throwable error) { } }); } + + /** + * Returns a Single that emits the item emitted by the source Single until an Observable emits an item. Upon + * emission of an item from {@code other}, this will emit a {@link CancellationException} rather than go to + * {@link SingleSubscriber#onSuccess(Object)}. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code takeUntil} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param other + * the Observable whose first emitted item will cause {@code takeUntil} to emit the item from the source + * Single + * @param + * the type of items emitted by {@code other} + * @return a Single that emits the item emitted by the source Single until such time as {@code other} emits + * its first item + * @see ReactiveX operators documentation: TakeUntil + */ + public final Single takeUntil(final Observable other) { + return lift(new Operator() { + @Override + public Subscriber call(Subscriber child) { + final Subscriber serial = new SerializedSubscriber(child, false); + + final Subscriber main = new Subscriber(serial, false) { + @Override + public void onNext(T t) { + serial.onNext(t); + } + @Override + public void onError(Throwable e) { + try { + serial.onError(e); + } finally { + serial.unsubscribe(); + } + } + @Override + public void onCompleted() { + try { + serial.onCompleted(); + } finally { + serial.unsubscribe(); + } + } + }; + + final Subscriber so = new Subscriber() { + + @Override + public void onCompleted() { + onError(new CancellationException("Stream was canceled before emitting a terminal event.")); + } + + @Override + public void onError(Throwable e) { + main.onError(e); + } + + @Override + public void onNext(E e) { + onError(new CancellationException("Stream was canceled before emitting a terminal event.")); + } + }; + + serial.add(main); + serial.add(so); + + child.add(serial); + + other.unsafeSubscribe(so); + + return main; + } + }); + } + + /** + * Returns a Single that emits the item emitted by the source Single until a second Single emits an item. Upon + * emission of an item from {@code other}, this will emit a {@link CancellationException} rather than go to + * {@link SingleSubscriber#onSuccess(Object)}. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code takeUntil} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param other + * the Single whose emitted item will cause {@code takeUntil} to emit the item from the source Single + * @param + * the type of item emitted by {@code other} + * @return a Single that emits the item emitted by the source Single until such time as {@code other} emits its item + * @see ReactiveX operators documentation: TakeUntil + */ + public final Single takeUntil(final Single other) { + return lift(new Operator() { + @Override + public Subscriber call(Subscriber child) { + final Subscriber serial = new SerializedSubscriber(child, false); + + final Subscriber main = new Subscriber(serial, false) { + @Override + public void onNext(T t) { + serial.onNext(t); + } + @Override + public void onError(Throwable e) { + try { + serial.onError(e); + } finally { + serial.unsubscribe(); + } + } + @Override + public void onCompleted() { + try { + serial.onCompleted(); + } finally { + serial.unsubscribe(); + } + } + }; + + final SingleSubscriber so = new SingleSubscriber() { + @Override + public void onSuccess(E value) { + onError(new CancellationException("Stream was canceled before emitting a terminal event.")); + } + + @Override + public void onError(Throwable e) { + main.onError(e); + } + }; + + serial.add(main); + serial.add(so); + + child.add(serial); + + other.subscribe(so); + + return main; + } + }); + } /** * Converts this Single into an {@link Observable}. diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 393088562c..0b934e65e6 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -15,6 +15,12 @@ import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + import rx.Single.OnSubscribe; import rx.exceptions.CompositeException; import rx.functions.*; @@ -22,13 +28,9 @@ import rx.schedulers.Schedulers; import rx.schedulers.TestScheduler; import rx.singles.BlockingSingle; +import rx.subjects.PublishSubject; import rx.subscriptions.Subscriptions; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; - import static org.junit.Assert.*; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; @@ -1353,4 +1355,362 @@ public Observable call(Throwable throwable) { int numberOfErrors = retryCounter.getOnErrorEvents().size(); assertEquals(retryCount, numberOfErrors); } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilSuccess() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestSingle other = new TestSingle(sOther); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Single.create(other)); + stringSingle.subscribe(result); + other.sendOnSuccess("one"); + + result.assertError(CancellationException.class); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilSourceSuccess() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestSingle other = new TestSingle(sOther); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Single.create(other)); + stringSingle.subscribe(result); + source.sendOnSuccess("one"); + + result.assertValue("one"); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilNext() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestObservable other = new TestObservable(sOther); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); + stringSingle.subscribe(result); + other.sendOnNext("one"); + + result.assertError(CancellationException.class); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilSourceSuccessObservable() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestObservable other = new TestObservable(sOther); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); + stringSingle.subscribe(result); + source.sendOnSuccess("one"); + + result.assertValue("one"); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilSourceError() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestSingle other = new TestSingle(sOther); + Throwable error = new Throwable(); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Single.create(other)); + stringSingle.subscribe(result); + source.sendOnError(error); + + result.assertError(error); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilSourceErrorObservable() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestObservable other = new TestObservable(sOther); + Throwable error = new Throwable(); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); + stringSingle.subscribe(result); + source.sendOnError(error); + + result.assertError(error); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilOtherError() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestSingle other = new TestSingle(sOther); + Throwable error = new Throwable(); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Single.create(other)); + stringSingle.subscribe(result); + other.sendOnError(error); + + result.assertError(error); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilOtherErrorObservable() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestObservable other = new TestObservable(sOther); + Throwable error = new Throwable(); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); + stringSingle.subscribe(result); + other.sendOnError(error); + + result.assertError(error); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + } + + @Test + @SuppressWarnings("unchecked") + public void takeUntilOtherCompleted() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestSingle source = new TestSingle(sSource); + TestObservable other = new TestObservable(sOther); + + TestSubscriber result = new TestSubscriber(); + Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); + stringSingle.subscribe(result); + other.sendOnCompleted(); + + result.assertError(CancellationException.class); + verify(sSource).unsubscribe(); + verify(sOther).unsubscribe(); + + } + + private static class TestObservable implements Observable.OnSubscribe { + + Observer observer = null; + Subscription s; + + public TestObservable(Subscription s) { + this.s = s; + } + + /* used to simulate subscription */ + public void sendOnCompleted() { + observer.onCompleted(); + } + + /* used to simulate subscription */ + public void sendOnNext(String value) { + observer.onNext(value); + } + + /* used to simulate subscription */ + public void sendOnError(Throwable e) { + observer.onError(e); + } + + @Override + public void call(Subscriber observer) { + this.observer = observer; + observer.add(s); + } + } + + private static class TestSingle implements Single.OnSubscribe { + + SingleSubscriber subscriber = null; + Subscription s; + + public TestSingle(Subscription s) { + this.s = s; + } + + /* used to simulate subscription */ + public void sendOnSuccess(String value) { + subscriber.onSuccess(value); + } + + /* used to simulate subscription */ + public void sendOnError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void call(SingleSubscriber observer) { + this.subscriber = observer; + observer.add(s); + } + } + + @Test + public void takeUntilFires() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + until.onNext(1); + + ts.assertError(CancellationException.class); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } + + @Test + public void takeUntilFiresObservable() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.take(1).toSingle().takeUntil(until.take(1)).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + until.onNext(1); + + ts.assertError(CancellationException.class); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } + + @Test + public void takeUntilDownstreamUnsubscribes() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.take(1).toSingle().takeUntil(until).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } + + @Test + public void takeUntilDownstreamUnsubscribesObservable() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); + + source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + } + + @Test + public void takeUntilSimple() { + PublishSubject stringSubject = PublishSubject.create(); + Single single = stringSubject.toSingle(); + + Subscription singleSubscription = single.takeUntil(Single.just("Hello")).subscribe( + new Action1() { + @Override + public void call(String s) { + fail(); + } + }, + new Action1() { + @Override + public void call(Throwable throwable) { + assertTrue(throwable instanceof CancellationException); + } + } + ); + assertTrue(singleSubscription.isUnsubscribed()); + } + + @Test + public void takeUntilObservable() { + PublishSubject stringSubject = PublishSubject.create(); + Single single = stringSubject.toSingle(); + PublishSubject otherSubject = PublishSubject.create(); + + Subscription singleSubscription = single.takeUntil(otherSubject.asObservable()).subscribe( + new Action1() { + @Override + public void call(String s) { + fail(); + } + }, + new Action1() { + @Override + public void call(Throwable throwable) { + assertTrue(throwable instanceof CancellationException); + } + } + ); + otherSubject.onNext("Hello"); + assertTrue(singleSubscription.isUnsubscribed()); + } } From bc9d82d9af6a060c63b3592febecf9f0f7a0f9bd Mon Sep 17 00:00:00 2001 From: Anatoly Korniltsev Date: Wed, 17 Feb 2016 15:39:18 +0400 Subject: [PATCH 531/857] Update information about jar size in README.md The latest version of rxjava is 978K long. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 789eb54959..33336aa681 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ RxJava is a Java VM implementation of [Reactive Extensions](http://reactivex.io) It extends the [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) to support sequences of data/events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety and concurrent data structures. - Zero Dependencies -- < 800KB Jar +- < 1MB Jar - Java 6+ & [Android](https://github.com/ReactiveX/RxAndroid) 2.3+ - Java 8 lambda support - Polyglot ([Scala](https://github.com/ReactiveX/RxScala), [Groovy](https://github.com/ReactiveX/RxGroovy), [Clojure](https://github.com/ReactiveX/RxClojure) and [Kotlin](https://github.com/ReactiveX/RxKotlin)) From 8794d0c87ef294c7db162be4bc78b7787a19df16 Mon Sep 17 00:00:00 2001 From: Said Tahsin Dane Date: Thu, 18 Feb 2016 15:49:19 +0200 Subject: [PATCH 532/857] Documentation fix. --- src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 64991569db..5e42dc830c 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -6319,7 +6319,7 @@ public final Observable onErrorReturn(Func1 resumeFun * encountered. *
    *
    Scheduler:
    - *
    {@code onErrorResumeNext} does not operate by default on a particular {@link Scheduler}.
    + *
    {@code onExceptionResumeNext} does not operate by default on a particular {@link Scheduler}.
    *
    * * @param resumeSequence From 1f8c2b3182217a8c0180f1f9e975c1d0385d67d3 Mon Sep 17 00:00:00 2001 From: Luka Cindro Date: Fri, 19 Feb 2016 14:42:02 +0100 Subject: [PATCH 533/857] 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 05bbf638eea73498c282beea66879e8b0eb52c33 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Mon, 22 Feb 2016 23:01:27 -0800 Subject: [PATCH 534/857] Add takeUntil(Completable) support and standardize tests --- src/main/java/rx/Single.java | 73 ++++++ src/test/java/rx/SingleTest.java | 413 +++++++++++++------------------ 2 files changed, 245 insertions(+), 241 deletions(-) diff --git a/src/main/java/rx/Single.java b/src/main/java/rx/Single.java index 813dc61a0d..20b983c063 100644 --- a/src/main/java/rx/Single.java +++ b/src/main/java/rx/Single.java @@ -1802,6 +1802,79 @@ public void onError(Throwable error) { }); } + /** + * Returns a Single that emits the item emitted by the source Single until a Completable terminates. Upon + * termination of {@code other}, this will emit a {@link CancellationException} rather than go to + * {@link SingleSubscriber#onSuccess(Object)}. + *

    + * + *

    + *
    Scheduler:
    + *
    {@code takeUntil} does not operate by default on a particular {@link Scheduler}.
    + *
    + * + * @param other + * the Completable whose termination will cause {@code takeUntil} to emit the item from the source + * Single + * @return a Single that emits the item emitted by the source Single until such time as {@code other} terminates. + * @see ReactiveX operators documentation: TakeUntil + */ + public final Single takeUntil(final Completable other) { + return lift(new Operator() { + @Override + public Subscriber call(Subscriber child) { + final Subscriber serial = new SerializedSubscriber(child, false); + + final Subscriber main = new Subscriber(serial, false) { + @Override + public void onNext(T t) { + serial.onNext(t); + } + @Override + public void onError(Throwable e) { + try { + serial.onError(e); + } finally { + serial.unsubscribe(); + } + } + @Override + public void onCompleted() { + try { + serial.onCompleted(); + } finally { + serial.unsubscribe(); + } + } + }; + + final Completable.CompletableSubscriber so = new Completable.CompletableSubscriber() { + @Override + public void onCompleted() { + onError(new CancellationException("Stream was canceled before emitting a terminal event.")); + } + + @Override + public void onError(Throwable e) { + main.onError(e); + } + + @Override + public void onSubscribe(Subscription d) { + serial.add(d); + } + }; + + serial.add(main); + child.add(serial); + + other.subscribe(so); + + return main; + } + }); + } + /** * Returns a Single that emits the item emitted by the source Single until an Observable emits an item. Upon * emission of an item from {@code other}, this will emit a {@link CancellationException} rather than go to diff --git a/src/test/java/rx/SingleTest.java b/src/test/java/rx/SingleTest.java index 0b934e65e6..3ce86e9772 100644 --- a/src/test/java/rx/SingleTest.java +++ b/src/test/java/rx/SingleTest.java @@ -1357,360 +1357,291 @@ public Observable call(Throwable throwable) { } @Test - @SuppressWarnings("unchecked") - public void takeUntilSuccess() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestSingle other = new TestSingle(sOther); + public void takeUntilCompletableFires() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Single.create(other)); - stringSingle.subscribe(result); - other.sendOnSuccess("one"); + TestSubscriber ts = new TestSubscriber(); - result.assertError(CancellationException.class); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); - } + source.take(1).toSingle().takeUntil(until.toCompletable()).unsafeSubscribe(ts); - @Test - @SuppressWarnings("unchecked") - public void takeUntilSourceSuccess() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestSingle other = new TestSingle(sOther); + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Single.create(other)); - stringSingle.subscribe(result); - source.sendOnSuccess("one"); + until.onCompleted(); + + ts.assertError(CancellationException.class); - result.assertValue("one"); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - @SuppressWarnings("unchecked") - public void takeUntilNext() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestObservable other = new TestObservable(sOther); + public void takeUntilObservableFires() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); - stringSingle.subscribe(result); - other.sendOnNext("one"); + TestSubscriber ts = new TestSubscriber(); - result.assertError(CancellationException.class); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); - } + source.take(1).toSingle().takeUntil(until.take(1)).unsafeSubscribe(ts); - @Test - @SuppressWarnings("unchecked") - public void takeUntilSourceSuccessObservable() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestObservable other = new TestObservable(sOther); + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); - stringSingle.subscribe(result); - source.sendOnSuccess("one"); + until.onNext(1); - result.assertValue("one"); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); + ts.assertError(CancellationException.class); + + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - @SuppressWarnings("unchecked") - public void takeUntilSourceError() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestSingle other = new TestSingle(sOther); - Throwable error = new Throwable(); + public void takeUntilSingleFires() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Single.create(other)); - stringSingle.subscribe(result); - source.sendOnError(error); + TestSubscriber ts = new TestSubscriber(); - result.assertError(error); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); - } + source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); - @Test - @SuppressWarnings("unchecked") - public void takeUntilSourceErrorObservable() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestObservable other = new TestObservable(sOther); - Throwable error = new Throwable(); + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + until.onNext(1); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); - stringSingle.subscribe(result); - source.sendOnError(error); + ts.assertError(CancellationException.class); - result.assertError(error); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - @SuppressWarnings("unchecked") - public void takeUntilOtherError() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestSingle other = new TestSingle(sOther); - Throwable error = new Throwable(); + public void takeUntilObservableCompletes() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Single.create(other)); - stringSingle.subscribe(result); - other.sendOnError(error); + TestSubscriber ts = new TestSubscriber(); - result.assertError(error); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); - } + source.take(1).toSingle().takeUntil(until.take(1)).unsafeSubscribe(ts); - @Test - @SuppressWarnings("unchecked") - public void takeUntilOtherErrorObservable() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestObservable other = new TestObservable(sOther); - Throwable error = new Throwable(); + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); - stringSingle.subscribe(result); - other.sendOnError(error); + until.onCompleted(); - result.assertError(error); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); + ts.assertError(CancellationException.class); + + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - @SuppressWarnings("unchecked") - public void takeUntilOtherCompleted() { - Subscription sSource = mock(Subscription.class); - Subscription sOther = mock(Subscription.class); - TestSingle source = new TestSingle(sSource); - TestObservable other = new TestObservable(sOther); + public void takeUntilSourceUnsubscribes_withCompletable() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); - TestSubscriber result = new TestSubscriber(); - Single stringSingle = Single.create(source).takeUntil(Observable.create(other)); - stringSingle.subscribe(result); - other.sendOnCompleted(); + source.take(1).toSingle().takeUntil(until.toCompletable()).unsafeSubscribe(ts); - result.assertError(CancellationException.class); - verify(sSource).unsubscribe(); - verify(sOther).unsubscribe(); + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminalEvent(); + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } - private static class TestObservable implements Observable.OnSubscribe { + @Test + public void takeUntilSourceUnsubscribes_withObservable() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); - Observer observer = null; - Subscription s; + TestSubscriber ts = new TestSubscriber(); - public TestObservable(Subscription s) { - this.s = s; - } + source.take(1).toSingle().takeUntil(until).unsafeSubscribe(ts); - /* used to simulate subscription */ - public void sendOnCompleted() { - observer.onCompleted(); - } + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); - /* used to simulate subscription */ - public void sendOnNext(String value) { - observer.onNext(value); - } + source.onNext(1); - /* used to simulate subscription */ - public void sendOnError(Throwable e) { - observer.onError(e); - } + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminalEvent(); - @Override - public void call(Subscriber observer) { - this.observer = observer; - observer.add(s); - } + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } - private static class TestSingle implements Single.OnSubscribe { + @Test + public void takeUntilSourceUnsubscribes_withSingle() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); + + TestSubscriber ts = new TestSubscriber(); - SingleSubscriber subscriber = null; - Subscription s; + source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); - public TestSingle(Subscription s) { - this.s = s; - } + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); - /* used to simulate subscription */ - public void sendOnSuccess(String value) { - subscriber.onSuccess(value); - } + source.onNext(1); - /* used to simulate subscription */ - public void sendOnError(Throwable e) { - subscriber.onError(e); - } + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminalEvent(); - @Override - public void call(SingleSubscriber observer) { - this.subscriber = observer; - observer.add(s); - } + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - public void takeUntilFires() { + public void takeUntilSourceErrorUnsubscribes_withCompletable() { PublishSubject source = PublishSubject.create(); PublishSubject until = PublishSubject.create(); TestSubscriber ts = new TestSubscriber(); - source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); + source.take(1).toSingle().takeUntil(until.toCompletable()).unsafeSubscribe(ts); assertTrue(source.hasObservers()); assertTrue(until.hasObservers()); - until.onNext(1); + Exception e = new Exception(); + source.onError(e); - ts.assertError(CancellationException.class); + ts.assertNoValues(); + ts.assertError(e); - assertFalse("Source still has observers", source.hasObservers()); - assertFalse("Until still has observers", until.hasObservers()); - assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - public void takeUntilFiresObservable() { + public void takeUntilSourceErrorUnsubscribes_withObservable() { PublishSubject source = PublishSubject.create(); PublishSubject until = PublishSubject.create(); TestSubscriber ts = new TestSubscriber(); - source.take(1).toSingle().takeUntil(until.take(1)).unsafeSubscribe(ts); + source.take(1).toSingle().takeUntil(until).unsafeSubscribe(ts); assertTrue(source.hasObservers()); assertTrue(until.hasObservers()); - until.onNext(1); + source.onError(new Throwable()); - ts.assertError(CancellationException.class); + ts.assertNoValues(); + ts.assertError(Throwable.class); - assertFalse("Source still has observers", source.hasObservers()); - assertFalse("Until still has observers", until.hasObservers()); - assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - public void takeUntilDownstreamUnsubscribes() { + public void takeUntilSourceErrorUnsubscribes_withSingle() { PublishSubject source = PublishSubject.create(); PublishSubject until = PublishSubject.create(); TestSubscriber ts = new TestSubscriber(); - source.take(1).toSingle().takeUntil(until).unsafeSubscribe(ts); + source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); assertTrue(source.hasObservers()); assertTrue(until.hasObservers()); - source.onNext(1); + source.onError(new Throwable()); - ts.assertValue(1); - ts.assertNoErrors(); - ts.assertTerminalEvent(); + ts.assertNoValues(); + ts.assertError(Throwable.class); - assertFalse("Source still has observers", source.hasObservers()); - assertFalse("Until still has observers", until.hasObservers()); - assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - public void takeUntilDownstreamUnsubscribesObservable() { + public void takeUntilError_withCompletable_shouldMatch() { PublishSubject source = PublishSubject.create(); PublishSubject until = PublishSubject.create(); TestSubscriber ts = new TestSubscriber(); - source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); + source.take(1).toSingle().takeUntil(until.toCompletable()).unsafeSubscribe(ts); assertTrue(source.hasObservers()); assertTrue(until.hasObservers()); - source.onNext(1); + Exception e = new Exception(); + until.onError(e); - ts.assertValue(1); - ts.assertNoErrors(); - ts.assertTerminalEvent(); + ts.assertNoValues(); + ts.assertError(e); - assertFalse("Source still has observers", source.hasObservers()); - assertFalse("Until still has observers", until.hasObservers()); - assertFalse("TestSubscriber is unsubscribed", ts.isUnsubscribed()); + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - public void takeUntilSimple() { - PublishSubject stringSubject = PublishSubject.create(); - Single single = stringSubject.toSingle(); + public void takeUntilError_withObservable_shouldMatch() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); - Subscription singleSubscription = single.takeUntil(Single.just("Hello")).subscribe( - new Action1() { - @Override - public void call(String s) { - fail(); - } - }, - new Action1() { - @Override - public void call(Throwable throwable) { - assertTrue(throwable instanceof CancellationException); - } - } - ); - assertTrue(singleSubscription.isUnsubscribed()); + TestSubscriber ts = new TestSubscriber(); + + source.take(1).toSingle().takeUntil(until.asObservable()).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + Exception e = new Exception(); + until.onError(e); + + ts.assertNoValues(); + ts.assertError(e); + + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } @Test - public void takeUntilObservable() { - PublishSubject stringSubject = PublishSubject.create(); - Single single = stringSubject.toSingle(); - PublishSubject otherSubject = PublishSubject.create(); + public void takeUntilError_withSingle_shouldMatch() { + PublishSubject source = PublishSubject.create(); + PublishSubject until = PublishSubject.create(); - Subscription singleSubscription = single.takeUntil(otherSubject.asObservable()).subscribe( - new Action1() { - @Override - public void call(String s) { - fail(); - } - }, - new Action1() { - @Override - public void call(Throwable throwable) { - assertTrue(throwable instanceof CancellationException); - } - } - ); - otherSubject.onNext("Hello"); - assertTrue(singleSubscription.isUnsubscribed()); + TestSubscriber ts = new TestSubscriber(); + + source.take(1).toSingle().takeUntil(until.take(1).toSingle()).unsafeSubscribe(ts); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + Exception e = new Exception(); + until.onError(e); + + ts.assertNoValues(); + ts.assertError(e); + + assertFalse(source.hasObservers()); + assertFalse(until.hasObservers()); + assertFalse(ts.isUnsubscribed()); } } From 457533ced4eca729bda542ec97b64af1d6c04af6 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Wed, 24 Feb 2016 09:25:03 +1100 Subject: [PATCH 535/857] scan should pass upstream a request of Long.MAX_VALUE (it should not decrement it) --- .../rx/internal/operators/OperatorScan.java | 6 +++++- .../internal/operators/OperatorScanTest.java | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/rx/internal/operators/OperatorScan.java b/src/main/java/rx/internal/operators/OperatorScan.java index ccf7a74c07..547edf5c1b 100644 --- a/src/main/java/rx/internal/operators/OperatorScan.java +++ b/src/main/java/rx/internal/operators/OperatorScan.java @@ -268,8 +268,12 @@ public void setProducer(Producer p) { if (producer != null) { throw new IllegalStateException("Can't set more than one Producer!"); } + mr = missedRequested; // request one less because of the initial value, this happens once - mr = missedRequested - 1; + // and is performed only if the request is not at MAX_VALUE already + if (mr != Long.MAX_VALUE) { + mr -= 1; + } missedRequested = 0L; producer = p; } diff --git a/src/test/java/rx/internal/operators/OperatorScanTest.java b/src/test/java/rx/internal/operators/OperatorScanTest.java index 20e53668a6..e45f32f92c 100644 --- a/src/test/java/rx/internal/operators/OperatorScanTest.java +++ b/src/test/java/rx/internal/operators/OperatorScanTest.java @@ -451,4 +451,22 @@ public void onNext(Integer t) { } }); } + + @Test + public void scanShouldPassUpstreamARequestForMaxValue() { + final List requests = new ArrayList(); + Observable.just(1,2,3).doOnRequest(new Action1() { + @Override + public void call(Long n) { + requests.add(n); + } + }) + .scan(new Func2() { + @Override + public Integer call(Integer t1, Integer t2) { + return 0; + }}).count().subscribe(); + + assertEquals(Arrays.asList(Long.MAX_VALUE), requests); + } } From 24400019a0c0b5eaa81b70a50fbf4f27935bedb6 Mon Sep 17 00:00:00 2001 From: ginbalin Date: Wed, 17 Feb 2016 20:08:55 +0100 Subject: [PATCH 536/857] add concatMapIterable --- src/main/java/rx/Observable.java | 23 +++++++++++++++ .../operators/OperatorConcatTest.java | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/main/java/rx/Observable.java b/src/main/java/rx/Observable.java index 64991569db..edc1a124d3 100644 --- a/src/main/java/rx/Observable.java +++ b/src/main/java/rx/Observable.java @@ -3889,6 +3889,29 @@ public final Observable concatMap(Func1 + * + *
    + *
    Scheduler:
    + *
    {@code concatMapIterable} 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 + * @return an Observable that emits the results of concatenating the items emitted by the source Observable with + * the values in the Iterables corresponding to those items, as generated by {@code collectionSelector} + * @see ReactiveX operators documentation: FlatMap + */ + public final Observable concatMapIterable(Func1> collectionSelector) { + return concat(map(OperatorMapPair.convertSelector(collectionSelector))); + } + /** * Returns an Observable that emits the items emitted from the current Observable, then the next, one after * the other, without interleaving them. diff --git a/src/test/java/rx/internal/operators/OperatorConcatTest.java b/src/test/java/rx/internal/operators/OperatorConcatTest.java index 8812dd50eb..a54c435432 100644 --- a/src/test/java/rx/internal/operators/OperatorConcatTest.java +++ b/src/test/java/rx/internal/operators/OperatorConcatTest.java @@ -82,6 +82,34 @@ public void testConcatWithList() { verify(observer, times(7)).onNext(anyString()); } + + @Test + public void testConcatMapIterable() { + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + + final String[] l = { "a", "b", "c", "d", "e" }; + + Func1,List> identity = new Func1, List>() { + @Override + public List call(List t) { + return t; + } + }; + + final Observable> listObs = Observable.just(Arrays.asList(l)); + final Observable concatMap = listObs.concatMapIterable(identity); + + concatMap.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("a"); + inOrder.verify(observer, times(1)).onNext("b"); + inOrder.verify(observer, times(1)).onNext("c"); + inOrder.verify(observer, times(1)).onNext("d"); + inOrder.verify(observer, times(1)).onNext("e"); + inOrder.verify(observer, times(1)).onCompleted(); + } @Test public void testConcatObservableOfObservables() { From 8b553035c737676efc61b0fc77e33f71ab84ea7f Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Wed, 24 Feb 2016 16:11:09 -0500 Subject: [PATCH 537/857] 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 538/857] 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 539/857] 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 540/857] 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 541/857] 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 542/857] 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 543/857] 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 544/857] 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 545/857] 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 546/857] 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 547/857] 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 548/857] 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 549/857] 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 550/857] 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 551/857] 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 552/857] 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 553/857] 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 554/857] 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 555/857] 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 556/857] 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 557/857] 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 558/857] 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 559/857] 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 560/857] 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 561/857] 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 562/857] 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 563/857] 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 564/857] 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 565/857] 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 566/857] 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 567/857] 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 568/857] 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 569/857] 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 570/857] 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 571/857] 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 572/857] 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 573/857] 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 574/857] 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 575/857] 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 576/857] 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 577/857] 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 578/857] 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 579/857] 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 580/857] 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 581/857] 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 582/857] 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 583/857] 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 584/857] 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 585/857] 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 586/857] 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 587/857] 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 588/857] 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 589/857] 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 590/857] 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 591/857] 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 592/857] 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 593/857] 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 594/857] 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 595/857] 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) { *