/* * Copyright 2017 Google 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 com.google.firebase.internal; import java.lang.Thread.UncaughtExceptionHandler; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Assert; import org.junit.Test; public class RevivingScheduledExecutorTest { private static final ThreadFactory THREAD_FACTORY = new ExceptionCatchingThreadFactory(); @Test public void testAppEngineRunnable() throws InterruptedException { final Semaphore semaphore = new Semaphore(0); final Set<Long> threadIds = new HashSet<>(); RevivingScheduledExecutor executor = new RevivingScheduledExecutor(THREAD_FACTORY, "testAppEngineRunnable", 0, 100); for (int i = 0; i < 50; ++i) { // We delay the execution to give the cleanup handler a chance to run. Otherwise, the // Executor's BlockingQueue will execute all Runnables before the internal thread gets // replaced. Thread.sleep(10); executor.execute( new Runnable() { @Override public void run() { threadIds.add(Thread.currentThread().getId()); semaphore.release(); } }); } try { Assert.assertTrue(semaphore.tryAcquire(50, 10, TimeUnit.SECONDS)); Assert.assertTrue(threadIds.size() > 1); } finally { executor.shutdownNow(); } } @Test public void testAppEnginePeriodicRunnable() throws InterruptedException { final Set<Long> threadIds = new HashSet<>(); final Semaphore semaphore = new Semaphore(0); RevivingScheduledExecutor executor = new RevivingScheduledExecutor(THREAD_FACTORY, "testAppEnginePeriodicRunnable", 0, 100); ScheduledFuture<?> future = executor.scheduleAtFixedRate( new Runnable() { @Override public void run() { threadIds.add(Thread.currentThread().getId()); semaphore.release(); } }, 0, 10, TimeUnit.MILLISECONDS); try { Assert.assertTrue(semaphore.tryAcquire(50, 10, TimeUnit.SECONDS)); Assert.assertTrue(threadIds.size() > 1); } finally { future.cancel(true); executor.shutdownNow(); } } @Test public void testAppEngineDelayedRunnable() throws InterruptedException { final Semaphore semaphore = new Semaphore(0); final AtomicInteger threads = new AtomicInteger(0); RevivingScheduledExecutor executor = new RevivingScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { threads.incrementAndGet(); return THREAD_FACTORY.newThread(r); } }, "testAppEngineDelayedRunnable", 0, 100); @SuppressWarnings("unused") Future<?> possiblyIgnoredError = executor.schedule( new Runnable() { @Override public void run() { semaphore.release(); } }, 750, TimeUnit.MILLISECONDS); try { Assert.assertFalse(semaphore.tryAcquire(1, 500, TimeUnit.MILLISECONDS)); Assert.assertTrue(semaphore.tryAcquire(1, 500, TimeUnit.MILLISECONDS)); Assert.assertTrue(threads.get() >= 2); } finally { executor.shutdownNow(); } } @Test public void testAppEngineDelayedCallable() throws InterruptedException, TimeoutException, ExecutionException { final AtomicInteger threads = new AtomicInteger(0); RevivingScheduledExecutor executor = new RevivingScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { threads.incrementAndGet(); return THREAD_FACTORY.newThread(r); } }, "testAppEngineDelayedCallable", 0, 100); ScheduledFuture<Boolean> future = executor.schedule( new Callable<Boolean>() { @Override public Boolean call() throws Exception { return true; } }, 750, TimeUnit.MILLISECONDS); try { Assert.assertTrue(future.get(1, TimeUnit.SECONDS)); Assert.assertTrue(threads.get() >= 2); } finally { executor.shutdownNow(); } } @Test public void testAppEngineCleanup() throws InterruptedException { final Semaphore beforeSemaphore = new Semaphore(0); final Semaphore afterSemaphore = new Semaphore(0); final AtomicInteger threads = new AtomicInteger(0); RevivingScheduledExecutor executor = new RevivingScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { threads.incrementAndGet(); return THREAD_FACTORY.newThread(r); } }, "testAppEngineCleanup", 0, 100) { @Override protected void beforeRestart() { beforeSemaphore.release(); } @Override protected void afterRestart() { afterSemaphore.release(); } }; @SuppressWarnings("unused") Future<?> possiblyIgnoredError = executor.submit( new Runnable() { @Override public void run() {} }); try { Assert.assertTrue(beforeSemaphore.tryAcquire(2, 10, TimeUnit.SECONDS)); Assert.assertTrue(afterSemaphore.tryAcquire(2, 10, TimeUnit.SECONDS)); Assert.assertEquals(3, threads.get()); Assert.assertEquals(0, beforeSemaphore.availablePermits()); Assert.assertEquals(0, afterSemaphore.availablePermits()); } finally { executor.shutdownNow(); } } private static class ExceptionCatchingThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { if (r == null) { return null; } Thread thread = Executors.defaultThreadFactory().newThread(r); thread.setUncaughtExceptionHandler( new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { // ignore -- to prevent the test output from getting cluttered } }); return thread; } } }