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(); + } + }