/** * 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.schedulers; 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 java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import rx.Observable; import rx.Observable.OnSubscribe; 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.subscriptions.Subscriptions; /** * Base tests for schedulers that involve threads (concurrency). * * These can only run on Schedulers that launch threads since they expect async/concurrent behavior. * * The Current/Immediate schedulers will not work with these tests. */ public abstract class AbstractSchedulerConcurrencyTests extends AbstractSchedulerTests { /** * Bug report: https://github.com/ReactiveX/RxJava/issues/431 */ @Test public final void testUnSubscribeForScheduler() throws InterruptedException { final AtomicInteger countReceived = new AtomicInteger(); final AtomicInteger countGenerated = new AtomicInteger(); final CountDownLatch latch = new CountDownLatch(1); Observable.interval(50, TimeUnit.MILLISECONDS) .map(new Func1<Long, Long>() { @Override public Long call(Long aLong) { countGenerated.incrementAndGet(); return aLong; } }) .subscribeOn(getScheduler()) .observeOn(getScheduler()) .subscribe(new Subscriber<Long>() { @Override public void onCompleted() { System.out.println("--- completed"); } @Override public void onError(Throwable e) { System.out.println("--- onError"); } @Override public void onNext(Long args) { if (countReceived.incrementAndGet() == 2) { unsubscribe(); latch.countDown(); } System.out.println("==> Received " + args); } }); latch.await(1000, TimeUnit.MILLISECONDS); System.out.println("----------- it thinks it is finished ------------------ "); Thread.sleep(100); assertEquals(2, countGenerated.get()); } @Test public void testUnsubscribeRecursiveScheduleFromOutside() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch unsubscribeLatch = new CountDownLatch(1); final AtomicInteger counter = new AtomicInteger(); final Worker inner = getScheduler().createWorker(); try { inner.schedule(new Action0() { @Override public void call() { inner.schedule(new Action0() { int i = 0; @Override public void call() { System.out.println("Run: " + i++); if (i == 10) { latch.countDown(); try { // wait for unsubscribe to finish so we are not racing it unsubscribeLatch.await(); } catch (InterruptedException e) { // we expect the countDown if unsubscribe is not working // or to be interrupted if unsubscribe is successful since // the unsubscribe will interrupt it as it is calling Future.cancel(true) // so we will ignore the stacktrace } } counter.incrementAndGet(); inner.schedule(this); } }); } }); latch.await(); inner.unsubscribe(); unsubscribeLatch.countDown(); Thread.sleep(200); // let time pass to see if the scheduler is still doing work assertEquals(10, counter.get()); } finally { inner.unsubscribe(); } } @Test public void testUnsubscribeRecursiveScheduleFromInside() throws InterruptedException { final CountDownLatch unsubscribeLatch = new CountDownLatch(1); final AtomicInteger counter = new AtomicInteger(); final Worker inner = getScheduler().createWorker(); try { inner.schedule(new Action0() { @Override public void call() { inner.schedule(new Action0() { int i = 0; @Override public void call() { System.out.println("Run: " + i++); if (i == 10) { inner.unsubscribe(); } counter.incrementAndGet(); inner.schedule(this); } }); } }); unsubscribeLatch.countDown(); Thread.sleep(200); // let time pass to see if the scheduler is still doing work assertEquals(10, counter.get()); } finally { inner.unsubscribe(); } } @Test public void testUnsubscribeRecursiveScheduleWithDelay() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch unsubscribeLatch = new CountDownLatch(1); final AtomicInteger counter = new AtomicInteger(); final Worker inner = getScheduler().createWorker(); try { inner.schedule(new Action0() { @Override public void call() { inner.schedule(new Action0() { long i = 1L; @Override public void call() { if (i++ == 10) { latch.countDown(); try { // wait for unsubscribe to finish so we are not racing it unsubscribeLatch.await(); } catch (InterruptedException e) { // we expect the countDown if unsubscribe is not working // or to be interrupted if unsubscribe is successful since // the unsubscribe will interrupt it as it is calling Future.cancel(true) // so we will ignore the stacktrace } } counter.incrementAndGet(); inner.schedule(this, 10, TimeUnit.MILLISECONDS); } }, 10, TimeUnit.MILLISECONDS); } }); latch.await(); inner.unsubscribe(); unsubscribeLatch.countDown(); Thread.sleep(200); // let time pass to see if the scheduler is still doing work assertEquals(10, counter.get()); } finally { inner.unsubscribe(); } } @Test public void recursionFromOuterActionAndUnsubscribeInside() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final Worker inner = getScheduler().createWorker(); try { inner.schedule(new Action0() { int i = 0; @Override public void call() { i++; if (i % 100000 == 0) { System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); } if (i < 1000000L) { inner.schedule(this); } else { latch.countDown(); } } }); latch.await(); } finally { inner.unsubscribe(); } } @Test public void testRecursion() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final Worker inner = getScheduler().createWorker(); try { inner.schedule(new Action0() { private long i = 0; @Override public void call() { i++; if (i % 100000 == 0) { System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); } if (i < 1000000L) { inner.schedule(this); } else { latch.countDown(); } } }); latch.await(); } finally { inner.unsubscribe(); } } @Test public void testRecursionAndOuterUnsubscribe() throws InterruptedException { // use latches instead of Thread.sleep final CountDownLatch latch = new CountDownLatch(10); final CountDownLatch completionLatch = new CountDownLatch(1); final Worker inner = getScheduler().createWorker(); try { Observable<Integer> obs = Observable.create(new OnSubscribe<Integer>() { @Override public void call(final Subscriber<? super Integer> observer) { inner.schedule(new Action0() { @Override public void call() { observer.onNext(42); latch.countDown(); // this will recursively schedule this task for execution again inner.schedule(this); } }); observer.add(Subscriptions.create(new Action0() { @Override public void call() { inner.unsubscribe(); observer.onCompleted(); completionLatch.countDown(); } })); } }); final AtomicInteger count = new AtomicInteger(); final AtomicBoolean completed = new AtomicBoolean(false); Subscription subscribe = obs.subscribe(new Subscriber<Integer>() { @Override public void onCompleted() { System.out.println("Completed"); completed.set(true); } @Override public void onError(Throwable e) { System.out.println("Error"); } @Override public void onNext(Integer args) { count.incrementAndGet(); System.out.println(args); } }); if (!latch.await(5000, TimeUnit.MILLISECONDS)) { fail("Timed out waiting on onNext latch"); } // now unsubscribe and ensure it stops the recursive loop subscribe.unsubscribe(); System.out.println("unsubscribe"); if (!completionLatch.await(5000, TimeUnit.MILLISECONDS)) { fail("Timed out waiting on completion latch"); } // the count can be 10 or higher due to thread scheduling of the unsubscribe vs the scheduler looping to emit the count assertTrue(count.get() >= 10); assertTrue(completed.get()); } finally { inner.unsubscribe(); } } @Test public final void testSubscribeWithScheduler() throws InterruptedException { final Scheduler scheduler = getScheduler(); final AtomicInteger count = new AtomicInteger(); Observable<Integer> o1 = Observable.<Integer> just(1, 2, 3, 4, 5); o1.subscribe(new Action1<Integer>() { @Override public void call(Integer t) { System.out.println("Thread: " + Thread.currentThread().getName()); System.out.println("t: " + t); count.incrementAndGet(); } }); // the above should be blocking so we should see a count of 5 assertEquals(5, count.get()); count.set(0); // now we'll subscribe with a scheduler and it should be async final String currentThreadName = Thread.currentThread().getName(); // latches for deterministically controlling the test below across threads final CountDownLatch latch = new CountDownLatch(5); final CountDownLatch first = new CountDownLatch(1); o1.subscribeOn(scheduler).subscribe(new Action1<Integer>() { @Override public void call(Integer t) { try { // we block the first one so we can assert this executes asynchronously with a count first.await(1000, TimeUnit.SECONDS); } catch (InterruptedException e) { throw new RuntimeException("The latch should have released if we are async.", e); } assertFalse(Thread.currentThread().getName().equals(currentThreadName)); System.out.println("Thread: " + Thread.currentThread().getName()); System.out.println("t: " + t); count.incrementAndGet(); latch.countDown(); } }); // assert we are async assertEquals(0, count.get()); // release the latch so it can go forward first.countDown(); // wait for all 5 responses latch.await(); assertEquals(5, count.get()); } }