/** * Copyright (c) 2016-present, RxJava Contributors. * * 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 io.reactivex.internal.operators.flowable; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import org.junit.*; import org.reactivestreams.*; import io.reactivex.*; import io.reactivex.internal.subscriptions.BooleanSubscription; import io.reactivex.subscribers.DefaultSubscriber; public class FlowableSerializeTest { Subscriber<String> observer; @Before public void before() { observer = TestHelper.mockSubscriber(); } @Test public void testSingleThreadedBasic() { TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable("one", "two", "three"); Flowable<String> w = Flowable.unsafeCreate(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)).onComplete(); // 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"); Flowable<String> w = Flowable.unsafeCreate(onSubscribe); BusyObserver busyobserver = new BusyObserver(); w.serialize().subscribe(busyobserver); onSubscribe.waitToFinish(); assertEquals(3, busyobserver.onNextCount.get()); assertFalse(busyobserver.onError); assertTrue(busyobserver.onComplete); // 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); Flowable<String> w = Flowable.unsafeCreate(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 onComplete because onError was invoked assertFalse(busyobserver.onComplete); // 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() { boolean lessThan9 = false; for (int i = 0; i < 3; i++) { TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); Flowable<String> w = Flowable.unsafeCreate(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 onComplete because onError was invoked assertFalse(busyobserver.onComplete); // 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. */ static class OnNextThread implements Runnable { private final DefaultSubscriber<String> observer; private final int numStringsToSend; OnNextThread(DefaultSubscriber<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. */ static class CompletionThread implements Runnable { private final DefaultSubscriber<String> observer; private final TestConcurrencyobserverEvent event; private final Future<?>[] waitOnThese; CompletionThread(DefaultSubscriber<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.onComplete) { observer.onComplete(); } else { throw new IllegalArgumentException("Expecting either onError or onComplete"); } } } enum TestConcurrencyobserverEvent { onComplete, onError, onNext } /** * This spawns a single thread for the subscribe execution. */ private static class TestSingleThreadedObservable implements Publisher<String> { final String[] values; private Thread t; TestSingleThreadedObservable(final String... values) { this.values = values; } @Override public void subscribe(final Subscriber<? super String> observer) { observer.onSubscribe(new BooleanSubscription()); 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.onComplete(); } 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 Publisher<String> { final String[] values; Thread t; AtomicInteger threadsRunning = new AtomicInteger(); AtomicInteger maxConcurrentThreads = new AtomicInteger(); ExecutorService threadPool; TestMultiThreadedObservable(String... values) { this.values = values; this.threadPool = Executors.newCachedThreadPool(); } @Override public void subscribe(final Subscriber<? super String> observer) { observer.onSubscribe(new BooleanSubscription()); System.out.println("TestMultiThreadedObservable subscribed to ..."); final NullPointerException npe = new NullPointerException(); 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 if (s == null) { System.out.println("TestMultiThreadedObservable onNext: null"); // force an error throw npe; } else { try { Thread.sleep(10); } catch (InterruptedException ex) { // ignored } System.out.println("TestMultiThreadedObservable onNext: " + s); } 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.onComplete(); } }); 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 DefaultSubscriber<String> { volatile boolean onComplete; volatile boolean onError; AtomicInteger onNextCount = new AtomicInteger(); AtomicInteger threadsRunning = new AtomicInteger(); AtomicInteger maxConcurrentThreads = new AtomicInteger(); @Override public void onComplete() { threadsRunning.incrementAndGet(); System.out.println(">>> Busyobserver received onComplete"); onComplete = 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(); } } } }