/** * 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.schedulers; import static org.junit.Assert.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import org.junit.Test; import org.reactivestreams.*; import io.reactivex.*; import io.reactivex.Scheduler.Worker; import io.reactivex.functions.*; import io.reactivex.subscribers.*; /** * 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 * @throws InterruptedException if the test is interrupted */ @Test public final void testUnSubscribeForScheduler() throws InterruptedException { final AtomicInteger countReceived = new AtomicInteger(); final AtomicInteger countGenerated = new AtomicInteger(); final CountDownLatch latch = new CountDownLatch(1); Flowable.interval(50, TimeUnit.MILLISECONDS) .map(new Function<Long, Long>() { @Override public Long apply(Long aLong) { countGenerated.incrementAndGet(); return aLong; } }) .subscribeOn(getScheduler()) .observeOn(getScheduler()) .subscribe(new DefaultSubscriber<Long>() { @Override public void onComplete() { System.out.println("--- completed"); } @Override public void onError(Throwable e) { System.out.println("--- onError"); } @Override public void onNext(Long args) { if (countReceived.incrementAndGet() == 2) { cancel(); latch.countDown(); } System.out.println("==> Received " + args); } }); latch.await(1000, TimeUnit.MILLISECONDS); System.out.println("----------- it thinks it is finished ------------------ "); int timeout = 10; while (timeout-- > 0 && countGenerated.get() != 2) { 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 Runnable() { @Override public void run() { inner.schedule(new Runnable() { int i; @Override public void run() { 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.dispose(); unsubscribeLatch.countDown(); Thread.sleep(200); // let time pass to see if the scheduler is still doing work assertEquals(10, counter.get()); } finally { inner.dispose(); } } @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 Runnable() { @Override public void run() { inner.schedule(new Runnable() { int i; @Override public void run() { System.out.println("Run: " + i++); if (i == 10) { inner.dispose(); } 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.dispose(); } } @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 Runnable() { @Override public void run() { inner.schedule(new Runnable() { long i = 1L; @Override public void run() { 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.dispose(); unsubscribeLatch.countDown(); Thread.sleep(200); // let time pass to see if the scheduler is still doing work assertEquals(10, counter.get()); } finally { inner.dispose(); } } @Test public void recursionFromOuterActionAndUnsubscribeInside() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final Worker inner = getScheduler().createWorker(); try { inner.schedule(new Runnable() { int i; @Override public void run() { 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.dispose(); } } @Test public void testRecursion() throws InterruptedException { final CountDownLatch latch = new CountDownLatch(1); final Worker inner = getScheduler().createWorker(); try { inner.schedule(new Runnable() { private long i; @Override public void run() { 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.dispose(); } } @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 { Flowable<Integer> obs = Flowable.unsafeCreate(new Publisher<Integer>() { @Override public void subscribe(final Subscriber<? super Integer> observer) { inner.schedule(new Runnable() { @Override public void run() { observer.onNext(42); latch.countDown(); // this will recursively schedule this task for execution again inner.schedule(this); } }); observer.onSubscribe(new Subscription() { @Override public void cancel() { inner.dispose(); observer.onComplete(); completionLatch.countDown(); } @Override public void request(long n) { } }); } }); final AtomicInteger count = new AtomicInteger(); final AtomicBoolean completed = new AtomicBoolean(false); ResourceSubscriber<Integer> s = new ResourceSubscriber<Integer>() { @Override public void onComplete() { 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); } }; obs.subscribe(s); if (!latch.await(5000, TimeUnit.MILLISECONDS)) { fail("Timed out waiting on onNext latch"); } // now unsubscribe and ensure it stops the recursive loop s.dispose(); 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.dispose(); } } @Test public final void testSubscribeWithScheduler() throws InterruptedException { final Scheduler scheduler = getScheduler(); final AtomicInteger count = new AtomicInteger(); Flowable<Integer> o1 = Flowable.<Integer> just(1, 2, 3, 4, 5); o1.subscribe(new Consumer<Integer>() { @Override public void accept(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 Consumer<Integer>() { @Override public void accept(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()); } }