/**
* 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 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.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 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.internal.util.RxRingBuffer;
import rx.observers.TestSubscriber;
import rx.schedulers.Schedulers;
import rx.schedulers.TestScheduler;
import rx.subscriptions.Subscriptions;
public class OperatorMergeTest {
@Mock
Observer<String> stringObserver;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testMergeObservableOfObservables() {
final Observable<String> o1 = Observable.create(new TestSynchronousObservable());
final Observable<String> o2 = Observable.create(new TestSynchronousObservable());
Observable<Observable<String>> observableOfObservables = Observable.create(new Observable.OnSubscribe<Observable<String>>() {
@Override
public void call(Subscriber<? super Observable<String>> observer) {
// simulate what would happen in an observable
observer.onNext(o1);
observer.onNext(o2);
observer.onCompleted();
}
});
Observable<String> m = Observable.merge(observableOfObservables);
m.subscribe(stringObserver);
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(1)).onCompleted();
verify(stringObserver, times(2)).onNext("hello");
}
@Test
public void testMergeArray() {
final Observable<String> o1 = Observable.create(new TestSynchronousObservable());
final Observable<String> o2 = Observable.create(new TestSynchronousObservable());
Observable<String> m = Observable.merge(o1, o2);
m.subscribe(stringObserver);
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(2)).onNext("hello");
verify(stringObserver, times(1)).onCompleted();
}
@Test
public void testMergeList() {
final Observable<String> o1 = Observable.create(new TestSynchronousObservable());
final Observable<String> o2 = Observable.create(new TestSynchronousObservable());
List<Observable<String>> listOfObservables = new ArrayList<Observable<String>>();
listOfObservables.add(o1);
listOfObservables.add(o2);
Observable<String> m = Observable.merge(listOfObservables);
m.subscribe(stringObserver);
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(1)).onCompleted();
verify(stringObserver, times(2)).onNext("hello");
}
@Test(timeout = 1000)
public void testUnSubscribeObservableOfObservables() throws InterruptedException {
final AtomicBoolean unsubscribed = new AtomicBoolean();
final CountDownLatch latch = new CountDownLatch(1);
Observable<Observable<Long>> source = Observable.create(new Observable.OnSubscribe<Observable<Long>>() {
@Override
public void call(final Subscriber<? super Observable<Long>> observer) {
// verbose on purpose so I can track the inside of it
final Subscription s = Subscriptions.create(new Action0() {
@Override
public void call() {
System.out.println("*** unsubscribed");
unsubscribed.set(true);
}
});
observer.add(s);
new Thread(new Runnable() {
@Override
public void run() {
while (!unsubscribed.get()) {
observer.onNext(Observable.just(1L, 2L));
}
System.out.println("Done looping after unsubscribe: " + unsubscribed.get());
observer.onCompleted();
// mark that the thread is finished
latch.countDown();
}
}).start();
}
});
final AtomicInteger count = new AtomicInteger();
Observable.merge(source).take(6).toBlocking().forEach(new Action1<Long>() {
@Override
public void call(Long v) {
System.out.println("Value: " + v);
int c = count.incrementAndGet();
if (c > 6) {
fail("Should be only 6");
}
}
});
latch.await(1000, TimeUnit.MILLISECONDS);
System.out.println("unsubscribed: " + unsubscribed.get());
assertTrue(unsubscribed.get());
}
@Test
public void testMergeArrayWithThreading() {
final TestASynchronousObservable o1 = new TestASynchronousObservable();
final TestASynchronousObservable o2 = new TestASynchronousObservable();
Observable<String> m = Observable.merge(Observable.create(o1), Observable.create(o2));
TestSubscriber<String> ts = new TestSubscriber<String>(stringObserver);
m.subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
verify(stringObserver, never()).onError(any(Throwable.class));
verify(stringObserver, times(2)).onNext("hello");
verify(stringObserver, times(1)).onCompleted();
}
@Test
public void testSynchronizationOfMultipleSequences() throws Throwable {
final TestASynchronousObservable o1 = new TestASynchronousObservable();
final TestASynchronousObservable o2 = new TestASynchronousObservable();
// use this latch to cause onNext to wait until we're ready to let it go
final CountDownLatch endLatch = new CountDownLatch(1);
final AtomicInteger concurrentCounter = new AtomicInteger();
final AtomicInteger totalCounter = new AtomicInteger();
Observable<String> m = Observable.merge(Observable.create(o1), Observable.create(o2));
m.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
throw new RuntimeException("failed", e);
}
@Override
public void onNext(String v) {
totalCounter.incrementAndGet();
concurrentCounter.incrementAndGet();
try {
// wait here until we're done asserting
endLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
throw new RuntimeException("failed", e);
} finally {
concurrentCounter.decrementAndGet();
}
}
});
// wait for both observables to send (one should be blocked)
o1.onNextBeingSent.await();
o2.onNextBeingSent.await();
// I can't think of a way to know for sure that both threads have or are trying to send onNext
// since I can't use a CountDownLatch for "after" onNext since I want to catch during it
// but I can't know for sure onNext is invoked
// so I'm unfortunately reverting to using a Thread.sleep to allow the process scheduler time
// to make sure after o1.onNextBeingSent and o2.onNextBeingSent are hit that the following
// onNext is invoked.
Thread.sleep(300);
try { // in try/finally so threads are released via latch countDown even if assertion fails
assertEquals(1, concurrentCounter.get());
} finally {
// release so it can finish
endLatch.countDown();
}
try {
o1.t.join();
o2.t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
assertEquals(2, totalCounter.get());
assertEquals(0, concurrentCounter.get());
}
/**
* unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge
*/
@Test
public void testError1() {
// we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior
final Observable<String> o1 = Observable.create(new TestErrorObservable("four", null, "six")); // we expect to lose "six"
final Observable<String> o2 = Observable.create(new TestErrorObservable("one", "two", "three")); // we expect to lose all of these since o1 is done first and fails
Observable<String> m = Observable.merge(o1, o2);
m.subscribe(stringObserver);
verify(stringObserver, times(1)).onError(any(NullPointerException.class));
verify(stringObserver, never()).onCompleted();
verify(stringObserver, times(0)).onNext("one");
verify(stringObserver, times(0)).onNext("two");
verify(stringObserver, times(0)).onNext("three");
verify(stringObserver, times(1)).onNext("four");
verify(stringObserver, times(0)).onNext("five");
verify(stringObserver, times(0)).onNext("six");
}
/**
* unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge
*/
@Test
public void testError2() {
// we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior
final Observable<String> o1 = Observable.create(new TestErrorObservable("one", "two", "three"));
final Observable<String> o2 = Observable.create(new TestErrorObservable("four", null, "six")); // we expect to lose "six"
final Observable<String> o3 = Observable.create(new TestErrorObservable("seven", "eight", null));// we expect to lose all of these since o2 is done first and fails
final Observable<String> o4 = Observable.create(new TestErrorObservable("nine"));// we expect to lose all of these since o2 is done first and fails
Observable<String> m = Observable.merge(o1, o2, o3, o4);
m.subscribe(stringObserver);
verify(stringObserver, times(1)).onError(any(NullPointerException.class));
verify(stringObserver, never()).onCompleted();
verify(stringObserver, times(1)).onNext("one");
verify(stringObserver, times(1)).onNext("two");
verify(stringObserver, times(1)).onNext("three");
verify(stringObserver, times(1)).onNext("four");
verify(stringObserver, times(0)).onNext("five");
verify(stringObserver, times(0)).onNext("six");
verify(stringObserver, times(0)).onNext("seven");
verify(stringObserver, times(0)).onNext("eight");
verify(stringObserver, times(0)).onNext("nine");
}
@Test
public void testThrownErrorHandling() {
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable<String> o1 = Observable.create(new OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> s) {
throw new RuntimeException("fail");
}
});
Observable.merge(o1, o1).subscribe(ts);
ts.awaitTerminalEvent(1000, TimeUnit.MILLISECONDS);
ts.assertTerminalEvent();
System.out.println("Error: " + ts.getOnErrorEvents());
}
private static class TestSynchronousObservable implements Observable.OnSubscribe<String> {
@Override
public void call(Subscriber<? super String> observer) {
observer.onNext("hello");
observer.onCompleted();
}
}
private static class TestASynchronousObservable implements Observable.OnSubscribe<String> {
Thread t;
final CountDownLatch onNextBeingSent = new CountDownLatch(1);
@Override
public void call(final Subscriber<? super String> observer) {
t = new Thread(new Runnable() {
@Override
public void run() {
onNextBeingSent.countDown();
try {
observer.onNext("hello");
// I can't use a countDownLatch to prove we are actually sending 'onNext'
// since it will block if synchronized and I'll deadlock
observer.onCompleted();
} catch (Exception e) {
observer.onError(e);
}
}
});
t.start();
}
}
private static class TestErrorObservable implements Observable.OnSubscribe<String> {
String[] valuesToReturn;
TestErrorObservable(String... values) {
valuesToReturn = values;
}
@Override
public void call(Subscriber<? super String> observer) {
for (String s : valuesToReturn) {
if (s == null) {
System.out.println("throwing exception");
observer.onError(new NullPointerException());
} else {
observer.onNext(s);
}
}
observer.onCompleted();
}
}
@Test
public void testUnsubscribeAsObservablesComplete() {
TestScheduler scheduler1 = Schedulers.test();
AtomicBoolean os1 = new AtomicBoolean(false);
Observable<Long> o1 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1);
TestScheduler scheduler2 = Schedulers.test();
AtomicBoolean os2 = new AtomicBoolean(false);
Observable<Long> o2 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2);
TestSubscriber<Long> ts = new TestSubscriber<Long>();
Observable.merge(o1, o2).subscribe(ts);
// we haven't incremented time so nothing should be received yet
ts.assertReceivedOnNext(Collections.<Long> emptyList());
scheduler1.advanceTimeBy(3, TimeUnit.SECONDS);
scheduler2.advanceTimeBy(2, TimeUnit.SECONDS);
ts.assertReceivedOnNext(Arrays.asList(0L, 1L, 2L, 0L, 1L));
// not unsubscribed yet
assertFalse(os1.get());
assertFalse(os2.get());
// advance to the end at which point it should complete
scheduler1.advanceTimeBy(3, TimeUnit.SECONDS);
ts.assertReceivedOnNext(Arrays.asList(0L, 1L, 2L, 0L, 1L, 3L, 4L));
assertTrue(os1.get());
assertFalse(os2.get());
// both should be completed now
scheduler2.advanceTimeBy(3, TimeUnit.SECONDS);
ts.assertReceivedOnNext(Arrays.asList(0L, 1L, 2L, 0L, 1L, 3L, 4L, 2L, 3L, 4L));
assertTrue(os1.get());
assertTrue(os2.get());
ts.assertTerminalEvent();
}
@Test
public void testEarlyUnsubscribe() {
for (int i = 0; i < 10; i++) {
TestScheduler scheduler1 = Schedulers.test();
AtomicBoolean os1 = new AtomicBoolean(false);
Observable<Long> o1 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1);
TestScheduler scheduler2 = Schedulers.test();
AtomicBoolean os2 = new AtomicBoolean(false);
Observable<Long> o2 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2);
TestSubscriber<Long> ts = new TestSubscriber<Long>();
Subscription s = Observable.merge(o1, o2).subscribe(ts);
// we haven't incremented time so nothing should be received yet
ts.assertReceivedOnNext(Collections.<Long> emptyList());
scheduler1.advanceTimeBy(3, TimeUnit.SECONDS);
scheduler2.advanceTimeBy(2, TimeUnit.SECONDS);
ts.assertReceivedOnNext(Arrays.asList(0L, 1L, 2L, 0L, 1L));
// not unsubscribed yet
assertFalse(os1.get());
assertFalse(os2.get());
// early unsubscribe
s.unsubscribe();
assertTrue(os1.get());
assertTrue(os2.get());
ts.assertReceivedOnNext(Arrays.asList(0L, 1L, 2L, 0L, 1L));
ts.assertUnsubscribed();
}
}
private Observable<Long> createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(final Scheduler scheduler, final AtomicBoolean unsubscribed) {
return Observable.create(new OnSubscribe<Long>() {
@Override
public void call(Subscriber<? super Long> s) {
s.add(Subscriptions.create(new Action0() {
@Override
public void call() {
unsubscribed.set(true);
}
}));
Observable.interval(1, TimeUnit.SECONDS, scheduler).take(5).subscribe(s);
}
});
}
@Test(timeout = 10000)
public void testConcurrency() {
Observable<Integer> o = Observable.range(1, 10000).subscribeOn(Schedulers.newThread());
for (int i = 0; i < 10; i++) {
Observable<Integer> merge = Observable.merge(o.onBackpressureBuffer(), o.onBackpressureBuffer(), o.onBackpressureBuffer());
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
merge.subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1, ts.getOnCompletedEvents().size());
List<Integer> onNextEvents = ts.getOnNextEvents();
assertEquals(30000, onNextEvents.size());
// System.out.println("onNext: " + onNextEvents.size() + " onCompleted: " + ts.getOnCompletedEvents().size());
}
}
@Test
public void testConcurrencyWithSleeping() {
Observable<Integer> o = Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(final Subscriber<? super Integer> s) {
Worker inner = Schedulers.newThread().createWorker();
s.add(inner);
inner.schedule(new Action0() {
@Override
public void call() {
try {
for (int i = 0; i < 100; i++) {
s.onNext(1);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
s.onError(e);
}
s.onCompleted();
}
});
}
});
for (int i = 0; i < 10; i++) {
Observable<Integer> merge = Observable.merge(o, o, o);
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
merge.subscribe(ts);
ts.awaitTerminalEvent();
assertEquals(1, ts.getOnCompletedEvents().size());
List<Integer> onNextEvents = ts.getOnNextEvents();
assertEquals(300, onNextEvents.size());
// System.out.println("onNext: " + onNextEvents.size() + " onCompleted: " + ts.getOnCompletedEvents().size());
}
}
@Test
public void testConcurrencyWithBrokenOnCompleteContract() {
Observable<Integer> o = Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(final Subscriber<? super Integer> s) {
Worker inner = Schedulers.newThread().createWorker();
s.add(inner);
inner.schedule(new Action0() {
@Override
public void call() {
try {
for (int i = 0; i < 10000; i++) {
s.onNext(i);
}
} catch (Exception e) {
s.onError(e);
}
s.onCompleted();
s.onCompleted();
s.onCompleted();
}
});
}
});
for (int i = 0; i < 10; i++) {
Observable<Integer> merge = Observable.merge(o.onBackpressureBuffer(), o.onBackpressureBuffer(), o.onBackpressureBuffer());
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
merge.subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1, ts.getOnCompletedEvents().size());
List<Integer> onNextEvents = ts.getOnNextEvents();
assertEquals(30000, onNextEvents.size());
// System.out.println("onNext: " + onNextEvents.size() + " onCompleted: " + ts.getOnCompletedEvents().size());
}
}
@Test
public void testBackpressureUpstream() throws InterruptedException {
final AtomicInteger generated1 = new AtomicInteger();
Observable<Integer> o1 = createInfiniteObservable(generated1).subscribeOn(Schedulers.computation());
final AtomicInteger generated2 = new AtomicInteger();
Observable<Integer> o2 = createInfiniteObservable(generated2).subscribeOn(Schedulers.computation());
TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() {
@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), o2.take(RxRingBuffer.SIZE * 2)).subscribe(testSubscriber);
testSubscriber.awaitTerminalEvent();
if (testSubscriber.getOnErrorEvents().size() > 0) {
testSubscriber.getOnErrorEvents().get(0).printStackTrace();
}
testSubscriber.assertNoErrors();
System.err.println(testSubscriber.getOnNextEvents());
assertEquals(RxRingBuffer.SIZE * 4, testSubscriber.getOnNextEvents().size());
// it should be between the take num and requested batch size across the async boundary
System.out.println("Generated 1: " + generated1.get());
System.out.println("Generated 2: " + generated2.get());
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();
Observable<Integer> o1 = createInfiniteObservable(generated1).subscribeOn(Schedulers.computation());
TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() {
@Override
public void onNext(Integer t) {
super.onNext(t);
}
};
Observable.merge(o1.take(RxRingBuffer.SIZE * 2), Observable.just(-99)).subscribe(testSubscriber);
testSubscriber.awaitTerminalEvent();
List<Integer> 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();
assertEquals(RxRingBuffer.SIZE * 2 + 1, onNextEvents.size());
// it should be between the take num and requested batch size across the async boundary
assertTrue(generated1.get() >= RxRingBuffer.SIZE * 2 && generated1.get() <= RxRingBuffer.SIZE * 3);
}
/**
* This is the same as the upstreams ones, but now adds the downstream as well by using observeOn.
*
* This requires merge to also obey the Product.request values coming from it's child subscriber.
*/
@Test
public void testBackpressureDownstreamWithConcurrentStreams() throws InterruptedException {
final AtomicInteger generated1 = new AtomicInteger();
Observable<Integer> o1 = createInfiniteObservable(generated1).subscribeOn(Schedulers.computation());
final AtomicInteger generated2 = new AtomicInteger();
Observable<Integer> o2 = createInfiniteObservable(generated2).subscribeOn(Schedulers.computation());
TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() {
@Override
public void onNext(Integer t) {
if (t < 100)
try {
// force a slow consumer
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread());
super.onNext(t);
}
};
Observable.merge(o1.take(RxRingBuffer.SIZE * 2), o2.take(RxRingBuffer.SIZE * 2)).observeOn(Schedulers.computation()).subscribe(testSubscriber);
testSubscriber.awaitTerminalEvent();
if (testSubscriber.getOnErrorEvents().size() > 0) {
testSubscriber.getOnErrorEvents().get(0).printStackTrace();
}
testSubscriber.assertNoErrors();
System.err.println(testSubscriber.getOnNextEvents());
assertEquals(RxRingBuffer.SIZE * 4, testSubscriber.getOnNextEvents().size());
// it should be between the take num and requested batch size across the async boundary
System.out.println("Generated 1: " + generated1.get());
System.out.println("Generated 2: " + generated2.get());
assertTrue(generated1.get() >= RxRingBuffer.SIZE * 2 && generated1.get() <= RxRingBuffer.SIZE * 4);
}
@Test
public void testBackpressureBothUpstreamAndDownstreamWithSynchronousScalarObservables() throws InterruptedException {
final AtomicInteger generated1 = new AtomicInteger();
Observable<Observable<Integer>> o1 = createInfiniteObservable(generated1).map(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(Integer t1) {
return Observable.just(t1);
}
});
TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() {
@Override
public void onNext(Integer t) {
if (t < 100)
try {
// force a slow consumer
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread());
super.onNext(t);
}
};
Observable.merge(o1).observeOn(Schedulers.computation()).take(RxRingBuffer.SIZE * 2).subscribe(testSubscriber);
testSubscriber.awaitTerminalEvent();
if (testSubscriber.getOnErrorEvents().size() > 0) {
testSubscriber.getOnErrorEvents().get(0).printStackTrace();
}
testSubscriber.assertNoErrors();
System.out.println("Generated 1: " + generated1.get());
System.err.println(testSubscriber.getOnNextEvents());
assertEquals(RxRingBuffer.SIZE * 2, testSubscriber.getOnNextEvents().size());
// it should be between the take num and requested batch size across the async boundary
assertTrue(generated1.get() >= RxRingBuffer.SIZE * 2 && generated1.get() <= RxRingBuffer.SIZE * 4);
}
/**
* Currently there is no solution to this ... we can't exert backpressure on the outer Observable if we
* can't know if the ones we've received so far are going to emit or not, otherwise we could starve the system.
*
* For example, 10,000 Observables are being merged (bad use case to begin with, but ...) and it's only one of them
* that will ever emit. If backpressure only allowed the first 1,000 to be sent, we would hang and never receive an event.
*
* Thus, we must allow all Observables to be sent. The ScalarSynchronousObservable use case is an exception to this since
* we can grab the value synchronously.
*
* @throws InterruptedException
*/
@Test(timeout = 5000)
public void testBackpressureBothUpstreamAndDownstreamWithRegularObservables() throws InterruptedException {
final AtomicInteger generated1 = new AtomicInteger();
Observable<Observable<Integer>> o1 = createInfiniteObservable(generated1).map(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(Integer t1) {
return Observable.just(1, 2, 3);
}
});
TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() {
int i = 0;
@Override
public void onNext(Integer t) {
if (i++ < 400)
try {
// force a slow consumer
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread());
super.onNext(t);
}
};
Observable.merge(o1).observeOn(Schedulers.computation()).take(RxRingBuffer.SIZE * 2).subscribe(testSubscriber);
testSubscriber.awaitTerminalEvent();
if (testSubscriber.getOnErrorEvents().size() > 0) {
testSubscriber.getOnErrorEvents().get(0).printStackTrace();
}
testSubscriber.assertNoErrors();
System.out.println("Generated 1: " + generated1.get());
System.err.println(testSubscriber.getOnNextEvents());
System.out.println("done1 testBackpressureBothUpstreamAndDownstreamWithRegularObservables ");
assertEquals(RxRingBuffer.SIZE * 2, testSubscriber.getOnNextEvents().size());
System.out.println("done2 testBackpressureBothUpstreamAndDownstreamWithRegularObservables ");
// we can't restrict this ... see comment above
// assertTrue(generated1.get() >= RxRingBuffer.SIZE && generated1.get() <= RxRingBuffer.SIZE * 4);
}
@Test
public void mergeWithNullValues() {
System.out.println("mergeWithNullValues");
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable.merge(Observable.just(null, "one"), Observable.just("two", null)).subscribe(ts);
ts.assertTerminalEvent();
ts.assertNoErrors();
ts.assertReceivedOnNext(Arrays.asList(null, "one", "two", null));
}
@Test
public void mergeWithTerminalEventAfterUnsubscribe() {
System.out.println("mergeWithTerminalEventAfterUnsubscribe");
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable<String> bad = Observable.create(new OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> s) {
s.onNext("two");
s.unsubscribe();
s.onCompleted();
}
});
Observable.merge(Observable.just(null, "one"), bad).subscribe(ts);
ts.assertNoErrors();
ts.assertReceivedOnNext(Arrays.asList(null, "one", "two"));
}
@Test
public void mergingNullObservable() {
TestSubscriber<String> ts = new TestSubscriber<String>();
Observable.merge(Observable.just("one"), null).subscribe(ts);
ts.assertNoErrors();
ts.assertReceivedOnNext(Arrays.asList("one"));
}
@Test
public void merge1AsyncStreamOf1() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNAsyncStreamsOfN(1, 1).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1, ts.getOnNextEvents().size());
}
@Test
public void merge1AsyncStreamOf1000() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNAsyncStreamsOfN(1, 1000).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1000, ts.getOnNextEvents().size());
}
@Test
public void merge10AsyncStreamOf1000() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNAsyncStreamsOfN(10, 1000).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(10000, ts.getOnNextEvents().size());
}
@Test
public void merge1000AsyncStreamOf1000() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNAsyncStreamsOfN(1000, 1000).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1000000, ts.getOnNextEvents().size());
}
@Test
public void merge2000AsyncStreamOf100() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNAsyncStreamsOfN(2000, 100).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(200000, ts.getOnNextEvents().size());
}
@Test
public void merge100AsyncStreamOf1() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNAsyncStreamsOfN(100, 1).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(100, ts.getOnNextEvents().size());
}
private Observable<Integer> mergeNAsyncStreamsOfN(final int outerSize, final int innerSize) {
Observable<Observable<Integer>> os = Observable.range(1, outerSize).map(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(Integer i) {
return Observable.range(1, innerSize).subscribeOn(Schedulers.computation());
}
});
return Observable.merge(os);
}
@Test
public void merge1SyncStreamOf1() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNSyncStreamsOfN(1, 1).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1, ts.getOnNextEvents().size());
}
@Test
public void merge1SyncStreamOf1000000() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNSyncStreamsOfN(1, 1000000).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1000000, ts.getOnNextEvents().size());
}
@Test
public void merge1000SyncStreamOf1000() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNSyncStreamsOfN(1000, 1000).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1000000, ts.getOnNextEvents().size());
}
@Test
public void merge10000SyncStreamOf10() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNSyncStreamsOfN(10000, 10).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(100000, ts.getOnNextEvents().size());
}
@Test
public void merge1000000SyncStreamOf1() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
mergeNSyncStreamsOfN(1000000, 1).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(1000000, ts.getOnNextEvents().size());
}
private Observable<Integer> mergeNSyncStreamsOfN(final int outerSize, final int innerSize) {
Observable<Observable<Integer>> os = Observable.range(1, outerSize).map(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(Integer i) {
return Observable.range(1, innerSize);
}
});
return Observable.merge(os);
}
private Observable<Integer> createInfiniteObservable(final AtomicInteger generated) {
Observable<Integer> observable = Observable.from(new Iterable<Integer>() {
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
@Override
public void remove() {
}
@Override
public Integer next() {
return generated.getAndIncrement();
}
@Override
public boolean hasNext() {
return true;
}
};
}
});
return observable;
}
@Test
public void mergeManyAsyncSingle() {
TestSubscriber<Integer> ts = new TestSubscriber<Integer>();
Observable<Observable<Integer>> os = Observable.range(1, 10000).map(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(final Integer i) {
return Observable.create(new OnSubscribe<Integer>() {
@Override
public void call(Subscriber<? super Integer> s) {
if (i < 500) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
s.onNext(i);
s.onCompleted();
}
}).subscribeOn(Schedulers.computation()).cache();
}
});
Observable.merge(os).subscribe(ts);
ts.awaitTerminalEvent();
ts.assertNoErrors();
assertEquals(10000, ts.getOnNextEvents().size());
}
@Test
public void shouldCompleteAfterApplyingBackpressure_NormalPath() {
Observable<Integer> source = Observable.mergeDelayError(Observable.just(Observable.range(1, 2)));
TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>();
subscriber.requestMore(0);
source.subscribe(subscriber);
subscriber.requestMore(3); // 1, 2, <complete> - with requestMore(2) we get the 1 and 2 but not the <complete>
subscriber.assertReceivedOnNext(asList(1, 2));
subscriber.assertTerminalEvent();
}
@Test
public void shouldCompleteAfterApplyingBackpressure_FastPath() {
Observable<Integer> source = Observable.mergeDelayError(Observable.just(Observable.just(1)));
TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>();
subscriber.requestMore(0);
source.subscribe(subscriber);
subscriber.requestMore(2); // 1, <complete> - should work as per .._NormalPath above
subscriber.assertReceivedOnNext(asList(1));
subscriber.assertTerminalEvent();
}
@Test
public void shouldNotCompleteIfThereArePendingScalarSynchronousEmissionsWhenTheLastInnerSubscriberCompletes() {
TestScheduler scheduler = Schedulers.test();
Observable<Long> source = Observable.mergeDelayError(Observable.just(1L), Observable.timer(1, TimeUnit.SECONDS, scheduler).skip(1));
TestSubscriber<Long> subscriber = new TestSubscriber<Long>();
subscriber.requestMore(0);
source.subscribe(subscriber);
scheduler.advanceTimeBy(1, TimeUnit.SECONDS);
subscriber.assertReceivedOnNext(Collections.<Long>emptyList());
assertEquals(Collections.<Notification<Long>>emptyList(), subscriber.getOnCompletedEvents());
subscriber.requestMore(1);
subscriber.assertReceivedOnNext(asList(1L));
assertEquals(Collections.<Notification<Long>>emptyList(), subscriber.getOnCompletedEvents());
subscriber.requestMore(1);
subscriber.assertTerminalEvent();
}
@Test
public void delayedErrorsShouldBeEmittedWhenCompleteAfterApplyingBackpressure_NormalPath() {
Throwable exception = new Throwable();
Observable<Integer> source = Observable.mergeDelayError(Observable.range(1, 2), Observable.<Integer>error(exception));
TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>();
subscriber.requestMore(0);
source.subscribe(subscriber);
subscriber.requestMore(3); // 1, 2, <error>
subscriber.assertReceivedOnNext(asList(1, 2));
subscriber.assertTerminalEvent();
assertEquals(asList(exception), subscriber.getOnErrorEvents());
}
@Test
public void delayedErrorsShouldBeEmittedWhenCompleteAfterApplyingBackpressure_FastPath() {
Throwable exception = new Throwable();
Observable<Integer> source = Observable.mergeDelayError(Observable.just(1), Observable.<Integer>error(exception));
TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>();
subscriber.requestMore(0);
source.subscribe(subscriber);
subscriber.requestMore(2); // 1, <error>
subscriber.assertReceivedOnNext(asList(1));
subscriber.assertTerminalEvent();
assertEquals(asList(exception), subscriber.getOnErrorEvents());
}
@Test
public void shouldNotCompleteWhileThereAreStillScalarSynchronousEmissionsInTheQueue() {
Observable<Integer> source = Observable.merge(Observable.just(1), Observable.just(2));
TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>();
subscriber.requestMore(1);
source.subscribe(subscriber);
subscriber.assertReceivedOnNext(asList(1));
subscriber.requestMore(1);
subscriber.assertReceivedOnNext(asList(1, 2));
}
@Test
public void shouldNotReceivedDelayedErrorWhileThereAreStillScalarSynchronousEmissionsInTheQueue() {
Throwable exception = new Throwable();
Observable<Integer> source = Observable.mergeDelayError(Observable.just(1), Observable.just(2), Observable.<Integer>error(exception));
TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>();
subscriber.requestMore(1);
source.subscribe(subscriber);
subscriber.assertReceivedOnNext(asList(1));
assertEquals(Collections.<Throwable>emptyList(), subscriber.getOnErrorEvents());
subscriber.requestMore(1);
subscriber.assertReceivedOnNext(asList(1, 2));
assertEquals(asList(exception), subscriber.getOnErrorEvents());
}
@Test
public void shouldNotReceivedDelayedErrorWhileThereAreStillNormalEmissionsInTheQueue() {
Throwable exception = new Throwable();
Observable<Integer> source = Observable.mergeDelayError(Observable.range(1, 2), Observable.range(3, 2), Observable.<Integer>error(exception));
TestSubscriber<Integer> subscriber = new TestSubscriber<Integer>();
subscriber.requestMore(3);
source.subscribe(subscriber);
subscriber.assertReceivedOnNext(asList(1, 2, 3));
assertEquals(Collections.<Throwable>emptyList(), subscriber.getOnErrorEvents());
subscriber.requestMore(2);
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<String> messages = new ConcurrentLinkedQueue<String>();
Observable.range(1, 2)
// produce many integers per second
.flatMap(new Func1<Integer, Observable<Integer>>() {
@Override
public Observable<Integer> call(final Integer number) {
return Observable.range(1, Integer.MAX_VALUE)
.doOnRequest(new Action1<Long>() {
@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<Long>() {
@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);
//}
}
@Test
public void testMergeRequestOverflow() throws InterruptedException {
//do a non-trivial merge so that future optimisations with EMPTY don't invalidate this test
Observable<Integer> 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<Integer>() {
@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<Integer> printCount() {
return new Action1<Integer>() {
long count;
@Override
public void call(Integer t1) {
count++;
System.out.println("count=" + count);
}
};
}
private static Action1<Integer> pauseForMs(final long time) {
return new Action1<Integer>() {
@Override
public void call(Integer s) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
}
}