/** * 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.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.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 rx.Observable.OnSubscribe; import rx.Observer; import rx.Subscriber; import rx.Subscription; import rx.schedulers.Schedulers; public class SerializedObserverTest { @Mock Subscriber<String> observer; @Before public void before() { MockitoAnnotations.initMocks(this); } private Observer<String> serializedObserver(Observer<String> o) { return new SerializedObserver<String>(o); } @Test public void testSingleThreadedBasic() { TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable("one", "two", "three"); Observable<String> w = Observable.create(onSubscribe); Observer<String> aw = serializedObserver(observer); w.subscribe(aw); 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(); Observer<String> aw = serializedObserver(busyObserver); w.subscribe(aw); 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(timeout = 1000) public void testMultiThreadedWithNPE() throws InterruptedException { TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null); Observable<String> w = Observable.create(onSubscribe); BusyObserver busyObserver = new BusyObserver(); Observer<String> aw = serializedObserver(busyObserver); w.subscribe(aw); onSubscribe.waitToFinish(); busyObserver.terminalEvent.await(); System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.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() { int n = 10; for (int i = 0; i < n; i++) { TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); Observable<String> w = Observable.create(onSubscribe); BusyObserver busyObserver = new BusyObserver(); Observer<String> aw = serializedObserver(busyObserver); w.subscribe(aw); onSubscribe.waitToFinish(); System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.maxConcurrentThreads.get()); // we can have concurrency ... assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); // ... but the onNext execution should be single threaded assertEquals(1, busyObserver.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()); assertFalse(busyObserver.onCompleted); assertTrue(busyObserver.onError); assertTrue(busyObserver.onNextCount.get() < 9); // no onCompleted because onError was invoked // 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(); } } /** * A non-realistic use case that tries to expose thread-safety issues by throwing lots of out-of-order * events on many threads. */ @Test public void runOutOfOrderConcurrencyTest() { ExecutorService tp = Executors.newFixedThreadPool(20); try { TestConcurrencyObserver tw = new TestConcurrencyObserver(); // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle Observer<String> w = serializedObserver(new SafeSubscriber<String>(tw)); Future<?> f1 = tp.submit(new OnNextThread(w, 12000)); Future<?> f2 = tp.submit(new OnNextThread(w, 5000)); Future<?> f3 = tp.submit(new OnNextThread(w, 75000)); Future<?> f4 = tp.submit(new OnNextThread(w, 13500)); Future<?> f5 = tp.submit(new OnNextThread(w, 22000)); Future<?> f6 = tp.submit(new OnNextThread(w, 15000)); Future<?> f7 = tp.submit(new OnNextThread(w, 7500)); Future<?> f8 = tp.submit(new OnNextThread(w, 23500)); Future<?> f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); try { Thread.sleep(1); } catch (InterruptedException e) { // ignore } Future<?> f11 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); Future<?> f12 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); Future<?> f13 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); Future<?> f14 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); // // the next 4 onError events should wait on same as f10 Future<?> f15 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); Future<?> f16 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); Future<?> f17 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); Future<?> f18 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); @SuppressWarnings("unused") int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior // System.out.println("Number of events executed: " + numNextEvents); } catch (Throwable e) { fail("Concurrency test failed: " + e.getMessage()); e.printStackTrace(); } finally { tp.shutdown(); try { tp.awaitTermination(5000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } } @Test public void runConcurrencyTest() { ExecutorService tp = Executors.newFixedThreadPool(20); try { TestConcurrencyObserver tw = new TestConcurrencyObserver(); // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle Observer<String> w = serializedObserver(new SafeSubscriber<String>(tw)); Future<?> f1 = tp.submit(new OnNextThread(w, 12000)); Future<?> f2 = tp.submit(new OnNextThread(w, 5000)); Future<?> f3 = tp.submit(new OnNextThread(w, 75000)); Future<?> f4 = tp.submit(new OnNextThread(w, 13500)); Future<?> f5 = tp.submit(new OnNextThread(w, 22000)); Future<?> f6 = tp.submit(new OnNextThread(w, 15000)); Future<?> f7 = tp.submit(new OnNextThread(w, 7500)); Future<?> f8 = tp.submit(new OnNextThread(w, 23500)); // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 Future<?> f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4, f5, f6, f7, f8)); try { Thread.sleep(1); } catch (InterruptedException e) { // ignore } waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior assertEquals(173500, numNextEvents); // System.out.println("Number of events executed: " + numNextEvents); } catch (Throwable e) { fail("Concurrency test failed: " + e.getMessage()); e.printStackTrace(); } finally { tp.shutdown(); try { tp.awaitTermination(25000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * Test that a notification does not get delayed in the queue waiting for the next event to push it through. * * @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); try { int n = 10; 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<String> to = new TestSubscriber<String>(new Observer<String>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @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) { } } }); Observer<String> 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()); } } finally { tp1.shutdown(); tp2.shutdown(); } } /** * Demonstrates thread starvation problem. * * No solution on this for now. Trade-off in this direction as per https://github.com/ReactiveX/RxJava/issues/998#issuecomment-38959474 * Probably need backpressure for this to work * * When using SynchronizedObserver we get this output: * * p1: 18 p2: 68 => should be close to each other unless we have thread starvation * * When using SerializedObserver we get: * * p1: 1 p2: 2445261 => should be close to each other unless we have thread starvation * * This demonstrates how SynchronizedObserver balances back and forth better, and blocks emission. * The real issue in this example is the async buffer-bloat, so we need backpressure. * * * @throws InterruptedException */ @Ignore @Test public void testThreadStarvation() throws InterruptedException { TestSubscriber<String> to = new TestSubscriber<String>(new Observer<String>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(String t) { // force it to take time when delivering try { Thread.sleep(1); } catch (InterruptedException e) { } } }); Observer<String> o = serializedObserver(to); AtomicInteger p1 = new AtomicInteger(); AtomicInteger p2 = new AtomicInteger(); Subscription s1 = infinite(p1).subscribe(o); Subscription s2 = infinite(p2).subscribe(o); Thread.sleep(100); System.out.println("p1: " + p1.get() + " p2: " + p2.get() + " => should be close to each other unless we have thread starvation"); assertEquals(p1.get(), p2.get(), 10000); // fairly distributed within 10000 of each other s1.unsubscribe(); s2.unsubscribe(); } private static void waitOnThreads(Future<?>... futures) { for (Future<?> f : futures) { try { f.get(20, TimeUnit.SECONDS); } catch (Throwable e) { System.err.println("Failed while waiting on future."); e.printStackTrace(); } } } private static Observable<String> infinite(final AtomicInteger produced) { return Observable.create(new OnSubscribe<String>() { @Override public void call(Subscriber<? super String> s) { while (!s.isUnsubscribed()) { s.onNext("onNext"); produced.incrementAndGet(); } } }).subscribeOn(Schedulers.newThread()); } /** * A thread that will pass data to onNext */ public static class OnNextThread implements Runnable { private final CountDownLatch latch; private final Observer<String> observer; private final int numStringsToSend; final AtomicInteger produced; private final CountDownLatch running; OnNextThread(Observer<String> observer, int numStringsToSend, CountDownLatch latch, CountDownLatch running) { this(observer, numStringsToSend, new AtomicInteger(), latch, running); } OnNextThread(Observer<String> observer, int numStringsToSend, AtomicInteger produced) { this(observer, numStringsToSend, produced, null, null); } OnNextThread(Observer<String> observer, int numStringsToSend, AtomicInteger produced, CountDownLatch latch, CountDownLatch running) { this.observer = observer; this.numStringsToSend = numStringsToSend; this.produced = produced; this.latch = latch; this.running = running; } OnNextThread(Observer<String> observer, int numStringsToSend) { this(observer, numStringsToSend, new AtomicInteger()); } @Override public void run() { if (running != null) { running.countDown(); } for (int i = 0; i < numStringsToSend; i++) { observer.onNext(Thread.currentThread().getId() + "-" + i); if (latch != null) { latch.countDown(); } produced.incrementAndGet(); } } } /** * 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 } private static class TestConcurrencyObserver extends Subscriber<String> { /** * used to store the order and number of events received */ private final LinkedBlockingQueue<TestConcurrencyObserverEvent> events = new LinkedBlockingQueue<TestConcurrencyObserverEvent>(); private final int waitTime; @SuppressWarnings("unused") public TestConcurrencyObserver(int waitTimeInNext) { this.waitTime = waitTimeInNext; } public TestConcurrencyObserver() { this.waitTime = 0; } @Override public void onCompleted() { events.add(TestConcurrencyObserverEvent.onCompleted); } @Override public void onError(Throwable e) { events.add(TestConcurrencyObserverEvent.onError); } @Override public void onNext(String args) { events.add(TestConcurrencyObserverEvent.onNext); // do some artificial work to make the thread scheduling/timing vary int s = 0; for (int i = 0; i < 20; i++) { s += s * i; } if (waitTime > 0) { try { Thread.sleep(waitTime); } catch (InterruptedException e) { // ignore } } } /** * Assert the order of events is correct and return the number of onNext executions. * * @param expectedEndingEvent * @return int count of onNext calls * @throws IllegalStateException * If order of events was invalid. */ public int assertEvents(TestConcurrencyObserverEvent expectedEndingEvent) throws IllegalStateException { int nextCount = 0; boolean finished = false; for (TestConcurrencyObserverEvent e : events) { if (e == TestConcurrencyObserverEvent.onNext) { if (finished) { // already finished, we shouldn't get this again throw new IllegalStateException("Received onNext but we're already finished."); } nextCount++; } else if (e == TestConcurrencyObserverEvent.onError) { if (finished) { // already finished, we shouldn't get this again throw new IllegalStateException("Received onError but we're already finished."); } if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onError != expectedEndingEvent) { throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); } finished = true; } else if (e == TestConcurrencyObserverEvent.onCompleted) { if (finished) { // already finished, we shouldn't get this again throw new IllegalStateException("Received onCompleted but we're already finished."); } if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onCompleted != expectedEndingEvent) { throw new IllegalStateException("Received onCompleted ending event but expected " + expectedEndingEvent); } finished = true; } } return nextCount; } } /** * 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"); int j = 0; for (final String s : values) { final int fj = ++j; threadPool.execute(new Runnable() { @Override public void run() { threadsRunning.incrementAndGet(); try { // perform onNext call System.out.println("TestMultiThreadedObservable onNext: " + s + " on thread " + Thread.currentThread().getName()); if (s == null) { // force an error throw new NullPointerException(); } else { // allow the exception to queue up int sleep = (fj % 3) * 10; if (sleep != 0) { Thread.sleep(sleep); } } 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 if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { System.out.println("Threadpool did not terminate in time."); } } 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(); final CountDownLatch terminalEvent = new CountDownLatch(1); @Override public void onCompleted() { threadsRunning.incrementAndGet(); try { onCompleted = true; } finally { captureMaxThreads(); threadsRunning.decrementAndGet(); terminalEvent.countDown(); } } @Override public void onError(Throwable e) { System.out.println(">>>>>>>>>>>>>>>>>>>> onError received: " + e); threadsRunning.incrementAndGet(); try { onError = true; } finally { captureMaxThreads(); threadsRunning.decrementAndGet(); terminalEvent.countDown(); } } @Override public void onNext(String args) { threadsRunning.incrementAndGet(); try { onNextCount.incrementAndGet(); try { // simulate doing something computational Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } finally { // capture 'maxThreads' captureMaxThreads(); threadsRunning.decrementAndGet(); } } protected void captureMaxThreads() { int concurrentThreads = threadsRunning.get(); int maxThreads = maxConcurrentThreads.get(); if (concurrentThreads > maxThreads) { maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); if (concurrentThreads > 1) { new RuntimeException("should not be greater than 1").printStackTrace(); } } } } }