/**
* 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.lang.management.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.*;
import io.reactivex.*;
import io.reactivex.Scheduler.Worker;
import io.reactivex.disposables.Disposable;
import io.reactivex.internal.disposables.EmptyDisposable;
import io.reactivex.internal.functions.Functions;
import io.reactivex.internal.schedulers.*;
import io.reactivex.plugins.RxJavaPlugins;
public class ExecutorSchedulerTest extends AbstractSchedulerConcurrencyTests {
static final Executor executor = Executors.newFixedThreadPool(2, new RxThreadFactory("TestCustomPool"));
@Override
protected Scheduler getScheduler() {
return Schedulers.from(executor);
}
@Test
@Ignore("Unhandled errors are no longer thrown")
public final void testUnhandledErrorIsDeliveredToThreadHandler() throws InterruptedException {
SchedulerTestHelper.testUnhandledErrorIsDeliveredToThreadHandler(getScheduler());
}
@Test
public final void testHandledErrorIsNotDeliveredToThreadHandler() throws InterruptedException {
SchedulerTestHelper.testHandledErrorIsNotDeliveredToThreadHandler(getScheduler());
}
public static void testCancelledRetention(Scheduler.Worker w, boolean periodic) throws InterruptedException {
System.out.println("Wait before GC");
Thread.sleep(1000);
System.out.println("GC");
System.gc();
Thread.sleep(1000);
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage();
long initial = memHeap.getUsed();
System.out.printf("Starting: %.3f MB%n", initial / 1024.0 / 1024.0);
int n = 100 * 1000;
if (periodic) {
final CountDownLatch cdl = new CountDownLatch(n);
final Runnable action = new Runnable() {
@Override
public void run() {
cdl.countDown();
}
};
for (int i = 0; i < n; i++) {
if (i % 50000 == 0) {
System.out.println(" -> still scheduling: " + i);
}
w.schedulePeriodically(action, 0, 1, TimeUnit.DAYS);
}
System.out.println("Waiting for the first round to finish...");
cdl.await();
} else {
for (int i = 0; i < n; i++) {
if (i % 50000 == 0) {
System.out.println(" -> still scheduling: " + i);
}
w.schedule(Functions.EMPTY_RUNNABLE, 1, TimeUnit.DAYS);
}
}
memHeap = memoryMXBean.getHeapMemoryUsage();
long after = memHeap.getUsed();
System.out.printf("Peak: %.3f MB%n", after / 1024.0 / 1024.0);
w.dispose();
System.out.println("Wait before second GC");
System.out.println("JDK 6 purge is N log N because it removes and shifts one by one");
int t = (int)(n * Math.log(n) / 100) + SchedulerPoolFactory.PURGE_PERIOD_SECONDS * 1000;
while (t > 0) {
System.out.printf(" >> Waiting for purge: %.2f s remaining%n", t / 1000d);
System.gc();
Thread.sleep(1000);
t -= 1000;
memHeap = memoryMXBean.getHeapMemoryUsage();
long finish = memHeap.getUsed();
System.out.printf("After: %.3f MB%n", finish / 1024.0 / 1024.0);
if (finish <= initial * 5) {
break;
}
}
System.out.println("Second GC");
System.gc();
Thread.sleep(1000);
memHeap = memoryMXBean.getHeapMemoryUsage();
long finish = memHeap.getUsed();
System.out.printf("After: %.3f MB%n", finish / 1024.0 / 1024.0);
if (finish > initial * 5) {
fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d));
}
}
@Test(timeout = 60000)
public void testCancelledTaskRetention() throws InterruptedException {
ExecutorService exec = Executors.newSingleThreadExecutor();
Scheduler s = Schedulers.from(exec);
try {
Scheduler.Worker w = s.createWorker();
try {
testCancelledRetention(w, false);
} finally {
w.dispose();
}
w = s.createWorker();
try {
testCancelledRetention(w, true);
} finally {
w.dispose();
}
} finally {
exec.shutdownNow();
}
}
/** A simple executor which queues tasks and executes them one-by-one if executeOne() is called. */
static final class TestExecutor implements Executor {
final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<Runnable>();
@Override
public void execute(Runnable command) {
queue.offer(command);
}
public void executeOne() {
Runnable r = queue.poll();
if (r != null) {
r.run();
}
}
public void executeAll() {
Runnable r;
while ((r = queue.poll()) != null) {
r.run();
}
}
}
@Test
public void testCancelledTasksDontRun() {
final AtomicInteger calls = new AtomicInteger();
Runnable task = new Runnable() {
@Override
public void run() {
calls.getAndIncrement();
}
};
TestExecutor exec = new TestExecutor();
Scheduler custom = Schedulers.from(exec);
Worker w = custom.createWorker();
try {
Disposable s1 = w.schedule(task);
Disposable s2 = w.schedule(task);
Disposable s3 = w.schedule(task);
s1.dispose();
s2.dispose();
s3.dispose();
exec.executeAll();
assertEquals(0, calls.get());
} finally {
w.dispose();
}
}
@Test
public void testCancelledWorkerDoesntRunTasks() {
final AtomicInteger calls = new AtomicInteger();
Runnable task = new Runnable() {
@Override
public void run() {
calls.getAndIncrement();
}
};
TestExecutor exec = new TestExecutor();
Scheduler custom = Schedulers.from(exec);
Worker w = custom.createWorker();
try {
w.schedule(task);
w.schedule(task);
w.schedule(task);
} finally {
w.dispose();
}
exec.executeAll();
assertEquals(0, calls.get());
}
// FIXME the internal structure changed and these can't be tested
//
// @Test
// public void testNoTimedTaskAfterScheduleRetention() throws InterruptedException {
// Executor e = new Executor() {
// @Override
// public void execute(Runnable command) {
// command.run();
// }
// };
// ExecutorWorker w = (ExecutorWorker)Schedulers.from(e).createWorker();
//
// w.schedule(Functions.emptyRunnable(), 50, TimeUnit.MILLISECONDS);
//
// assertTrue(w.tasks.hasSubscriptions());
//
// Thread.sleep(150);
//
// assertFalse(w.tasks.hasSubscriptions());
// }
//
// @Test
// public void testNoTimedTaskPartRetention() {
// Executor e = new Executor() {
// @Override
// public void execute(Runnable command) {
//
// }
// };
// ExecutorWorker w = (ExecutorWorker)Schedulers.from(e).createWorker();
//
// Disposable s = w.schedule(Functions.emptyRunnable(), 1, TimeUnit.DAYS);
//
// assertTrue(w.tasks.hasSubscriptions());
//
// s.dispose();
//
// assertFalse(w.tasks.hasSubscriptions());
// }
//
// @Test
// public void testNoPeriodicTimedTaskPartRetention() throws InterruptedException {
// Executor e = new Executor() {
// @Override
// public void execute(Runnable command) {
// command.run();
// }
// };
// ExecutorWorker w = (ExecutorWorker)Schedulers.from(e).createWorker();
//
// final CountDownLatch cdl = new CountDownLatch(1);
// final Runnable action = new Runnable() {
// @Override
// public void run() {
// cdl.countDown();
// }
// };
//
// Disposable s = w.schedulePeriodically(action, 0, 1, TimeUnit.DAYS);
//
// assertTrue(w.tasks.hasSubscriptions());
//
// cdl.await();
//
// s.dispose();
//
// assertFalse(w.tasks.hasSubscriptions());
// }
@Test
public void plainExecutor() throws Exception {
Scheduler s = Schedulers.from(new Executor() {
@Override
public void execute(Runnable r) {
r.run();
}
});
final CountDownLatch cdl = new CountDownLatch(5);
Runnable r = new Runnable() {
@Override
public void run() {
cdl.countDown();
}
};
s.scheduleDirect(r);
s.scheduleDirect(r, 50, TimeUnit.MILLISECONDS);
Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS);
try {
assertTrue(cdl.await(5, TimeUnit.SECONDS));
} finally {
d.dispose();
}
assertTrue(d.isDisposed());
}
@Test
public void rejectingExecutor() {
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
exec.shutdown();
Scheduler s = Schedulers.from(exec);
List<Throwable> errors = TestHelper.trackPluginErrors();
try {
assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE));
assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS));
assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS));
TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class);
TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class);
TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class);
} finally {
RxJavaPlugins.reset();
}
}
@Test
public void rejectingExecutorWorker() {
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
exec.shutdown();
List<Throwable> errors = TestHelper.trackPluginErrors();
try {
Worker s = Schedulers.from(exec).createWorker();
assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE));
s = Schedulers.from(exec).createWorker();
assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS));
s = Schedulers.from(exec).createWorker();
assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodically(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS));
TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class);
TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class);
TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class);
} finally {
RxJavaPlugins.reset();
}
}
@Test
public void reuseScheduledExecutor() throws Exception {
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
try {
Scheduler s = Schedulers.from(exec);
final CountDownLatch cdl = new CountDownLatch(8);
Runnable r = new Runnable() {
@Override
public void run() {
cdl.countDown();
}
};
s.scheduleDirect(r);
s.scheduleDirect(r, 10, TimeUnit.MILLISECONDS);
Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS);
try {
assertTrue(cdl.await(5, TimeUnit.SECONDS));
} finally {
d.dispose();
}
} finally {
exec.shutdown();
}
}
@Test
public void reuseScheduledExecutorAsWorker() throws Exception {
ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor();
Worker s = Schedulers.from(exec).createWorker();
assertFalse(s.isDisposed());
try {
final CountDownLatch cdl = new CountDownLatch(8);
Runnable r = new Runnable() {
@Override
public void run() {
cdl.countDown();
}
};
s.schedule(r);
s.schedule(r, 10, TimeUnit.MILLISECONDS);
Disposable d = s.schedulePeriodically(r, 10, 10, TimeUnit.MILLISECONDS);
try {
assertTrue(cdl.await(5, TimeUnit.SECONDS));
} finally {
d.dispose();
}
} finally {
s.dispose();
exec.shutdown();
}
assertTrue(s.isDisposed());
}
@Test
public void disposeRace() {
ExecutorService exec = Executors.newSingleThreadExecutor();
final Scheduler s = Schedulers.from(exec);
try {
for (int i = 0; i < 500; i++) {
final Worker w = s.createWorker();
final AtomicInteger c = new AtomicInteger(2);
w.schedule(new Runnable() {
@Override
public void run() {
c.decrementAndGet();
while (c.get() != 0) { }
}
});
c.decrementAndGet();
while (c.get() != 0) { }
w.dispose();
}
} finally {
exec.shutdownNow();
}
}
@Test
public void runnableDisposed() {
final Scheduler s = Schedulers.from(new Executor() {
@Override
public void execute(Runnable r) {
r.run();
}
});
Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE);
assertTrue(d.isDisposed());
}
@Test(timeout = 1000)
public void runnableDisposedAsync() throws Exception {
final Scheduler s = Schedulers.from(new Executor() {
@Override
public void execute(Runnable r) {
new Thread(r).start();
}
});
Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE);
while (!d.isDisposed()) {
Thread.sleep(1);
}
}
@Test(timeout = 1000)
public void runnableDisposedAsync2() throws Exception {
final Scheduler s = Schedulers.from(executor);
Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE);
while (!d.isDisposed()) {
Thread.sleep(1);
}
}
@Test(timeout = 1000)
public void runnableDisposedAsyncCrash() throws Exception {
final Scheduler s = Schedulers.from(new Executor() {
@Override
public void execute(Runnable r) {
new Thread(r).start();
}
});
Disposable d = s.scheduleDirect(new Runnable() {
@Override
public void run() {
throw new IllegalStateException();
}
});
while (!d.isDisposed()) {
Thread.sleep(1);
}
}
@Test(timeout = 1000)
public void runnableDisposedAsyncTimed() throws Exception {
final Scheduler s = Schedulers.from(new Executor() {
@Override
public void execute(Runnable r) {
new Thread(r).start();
}
});
Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS);
while (!d.isDisposed()) {
Thread.sleep(1);
}
}
@Test(timeout = 1000)
public void runnableDisposedAsyncTimed2() throws Exception {
ExecutorService executorScheduler = Executors.newScheduledThreadPool(1, new RxThreadFactory("TestCustomPoolTimed"));
try {
final Scheduler s = Schedulers.from(executorScheduler);
Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS);
while (!d.isDisposed()) {
Thread.sleep(1);
}
} finally {
executorScheduler.shutdownNow();
}
}
}