/** * 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.assertFalse; import static org.junit.Assert.assertTrue; 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.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; 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; public class OperatorSerializeTest { @Mock Observer<String> observer; @Before public void before() { MockitoAnnotations.initMocks(this); } @Test public void testSingleThreadedBasic() { TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable("one", "two", "three"); Observable<String> w = Observable.create(onSubscribe); w.serialize().subscribe(observer); onSubscribe.waitToFinish(); 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(); // 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(); } @Test public void testMultiThreadedBasic() { TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three"); Observable<String> w = Observable.create(onSubscribe); BusyObserver busyobserver = new BusyObserver(); w.serialize().subscribe(busyobserver); onSubscribe.waitToFinish(); assertEquals(3, busyobserver.onNextCount.get()); assertFalse(busyobserver.onError); assertTrue(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()); } @Test public void testMultiThreadedWithNPE() { TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null); Observable<String> w = Observable.create(onSubscribe); BusyObserver busyobserver = new BusyObserver(); w.serialize().subscribe(busyobserver); onSubscribe.waitToFinish(); System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); // we can't know how many onNext calls will occur since they each run on a separate thread // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options // assertEquals(3, busyobserver.onNextCount.get()); assertTrue(busyobserver.onNextCount.get() < 4); 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()); } @Test public void testMultiThreadedWithNPEinMiddle() { TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); Observable<String> 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()); } /** * A thread that will pass data to onNext */ public static class OnNextThread implements Runnable { private final Observer<String> observer; private final int numStringsToSend; OnNextThread(Observer<String> observer, int numStringsToSend) { this.observer = observer; this.numStringsToSend = numStringsToSend; } @Override public void run() { for (int i = 0; i < numStringsToSend; i++) { observer.onNext("aString"); } } } /** * A thread that will call onError or onNext */ public static class CompletionThread implements Runnable { private final Observer<String> observer; private final TestConcurrencyobserverEvent event; private final Future<?>[] waitOnThese; CompletionThread(Observer<String> observer, TestConcurrencyobserverEvent event, Future<?>... waitOnThese) { this.observer = observer; this.event = event; this.waitOnThese = waitOnThese; } @Override public void run() { /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ if (waitOnThese != null) { for (Future<?> f : waitOnThese) { try { f.get(); } catch (Throwable e) { System.err.println("Error while waiting on future in CompletionThread"); } } } /* send the event */ if (event == TestConcurrencyobserverEvent.onError) { observer.onError(new RuntimeException("mocked exception")); } else if (event == TestConcurrencyobserverEvent.onCompleted) { observer.onCompleted(); } else { throw new IllegalArgumentException("Expecting either onError or onCompleted"); } } } private static enum TestConcurrencyobserverEvent { onCompleted, onError, onNext } /** * This spawns a single thread for the subscribe execution */ private static class TestSingleThreadedObservable implements Observable.OnSubscribe<String> { final String[] values; private Thread t = null; public TestSingleThreadedObservable(final String... values) { this.values = values; } @Override public void call(final Subscriber<? super String> observer) { System.out.println("TestSingleThreadedObservable subscribed to ..."); t = new Thread(new Runnable() { @Override public void run() { try { System.out.println("running TestSingleThreadedObservable thread"); for (String s : values) { System.out.println("TestSingleThreadedObservable onNext: " + s); observer.onNext(s); } observer.onCompleted(); } catch (Throwable e) { throw new RuntimeException(e); } } }); System.out.println("starting TestSingleThreadedObservable thread"); t.start(); System.out.println("done starting TestSingleThreadedObservable thread"); } public void waitToFinish() { try { t.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } /** * This spawns a thread for the subscription, then a separate thread for each onNext call. */ private static class TestMultiThreadedObservable implements Observable.OnSubscribe<String> { final String[] values; Thread t = null; AtomicInteger threadsRunning = new AtomicInteger(); AtomicInteger maxConcurrentThreads = new AtomicInteger(); ExecutorService threadPool; public TestMultiThreadedObservable(String... values) { this.values = values; this.threadPool = Executors.newCachedThreadPool(); } @Override public void call(final Subscriber<? super String> observer) { System.out.println("TestMultiThreadedObservable subscribed to ..."); t = new Thread(new Runnable() { @Override public void run() { try { System.out.println("running TestMultiThreadedObservable thread"); for (final String s : values) { threadPool.execute(new Runnable() { @Override public void run() { threadsRunning.incrementAndGet(); try { // perform onNext call System.out.println("TestMultiThreadedObservable onNext: " + s); if (s == null) { // force an error throw new NullPointerException(); } observer.onNext(s); // capture 'maxThreads' int concurrentThreads = threadsRunning.get(); int maxThreads = maxConcurrentThreads.get(); if (concurrentThreads > maxThreads) { maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); } } catch (Throwable e) { observer.onError(e); } finally { threadsRunning.decrementAndGet(); } } }); } // we are done spawning threads threadPool.shutdown(); } catch (Throwable e) { throw new RuntimeException(e); } // wait until all threads are done, then mark it as COMPLETED try { // wait for all the threads to finish threadPool.awaitTermination(2, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException(e); } observer.onCompleted(); } }); System.out.println("starting TestMultiThreadedObservable thread"); t.start(); System.out.println("done starting TestMultiThreadedObservable thread"); } public void waitToFinish() { try { t.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } private static class BusyObserver extends Subscriber<String> { volatile boolean onCompleted = false; volatile boolean onError = false; AtomicInteger onNextCount = new AtomicInteger(); AtomicInteger threadsRunning = new AtomicInteger(); AtomicInteger maxConcurrentThreads = new AtomicInteger(); @Override public void onCompleted() { threadsRunning.incrementAndGet(); System.out.println(">>> Busyobserver received onCompleted"); onCompleted = true; int concurrentThreads = threadsRunning.get(); int maxThreads = maxConcurrentThreads.get(); if (concurrentThreads > maxThreads) { maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); } threadsRunning.decrementAndGet(); } @Override public void onError(Throwable e) { threadsRunning.incrementAndGet(); System.out.println(">>> Busyobserver received onError: " + e.getMessage()); onError = true; int concurrentThreads = threadsRunning.get(); int maxThreads = maxConcurrentThreads.get(); if (concurrentThreads > maxThreads) { maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); } threadsRunning.decrementAndGet(); } @Override public void onNext(String args) { threadsRunning.incrementAndGet(); try { onNextCount.incrementAndGet(); System.out.println(">>> Busyobserver received onNext: " + args); try { // simulate doing something computational Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } finally { // capture 'maxThreads' int concurrentThreads = threadsRunning.get(); int maxThreads = maxConcurrentThreads.get(); if (concurrentThreads > maxThreads) { maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); } threadsRunning.decrementAndGet(); } } } }