/*
* 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 com.google.common.annotations.VisibleForTesting;
import java.security.AccessControlException;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.RunnableScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* RevivingScheduledExecutor is an implementation of ScheduledThreadPoolExecutor that uses one
* periodically restarting worker thread as its work queue. This allows customers of this class to
* use this executor on App Engine despite App Engine's thread-lifetime limitations.
*/
public class RevivingScheduledExecutor extends ScheduledThreadPoolExecutor {
private static final String TAG = "RevivingScheduledExecutor";
/** Exception to throw to shut down the core threads. */
private static final RuntimeException REVIVE_THREAD_EXCEPTION =
new RuntimeException("Restarting Firebase Worker Thread");
/** The lifetime of a thread. Maximum lifetime of a thread on GAE is 24 hours. */
private static final long PERIODIC_RESTART_INTERVAL_MS = TimeUnit.HOURS.toMillis(12);
/**
* Time by which we offset restarts to ensure that not all threads die at the same time. This is
* meant to decrease cross-thread liveliness issues during restarts.
*/
private static final long PERIODIC_RESTART_OFFSET_MS = TimeUnit.MINUTES.toMillis(5);
private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0);
private final long initialDelayMs;
private final long timeoutMs;
// Flag set before throwing a REVIVE_THREAD_EXCEPTION and unset once a new thread has been
// created. Used to call afterRestart() appropriately.
private AtomicBoolean requestedRestart = new AtomicBoolean();
/**
* Creates a new RevivingScheduledExecutor that optionally restarts its worker thread every twelve
* hours.
*
* @param threadFactory Thread factory to use to restart threads.
* @param threadName Name of the threads in the pool.
* @param periodicRestart Periodically restart its worked threads.
*/
public RevivingScheduledExecutor(
final ThreadFactory threadFactory, final String threadName, final boolean periodicRestart) {
this(
threadFactory,
threadName,
periodicRestart ? PERIODIC_RESTART_OFFSET_MS * INSTANCE_COUNTER.get() : 0,
periodicRestart ? PERIODIC_RESTART_INTERVAL_MS : -1);
}
@VisibleForTesting
RevivingScheduledExecutor(
final ThreadFactory threadFactory,
final String threadName,
final long initialDelayMs,
final long timeoutMs) {
super(0);
INSTANCE_COUNTER.incrementAndGet();
this.initialDelayMs = initialDelayMs;
this.timeoutMs = timeoutMs;
setThreadFactory(
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Log.d(TAG, "Creating new thread for: " + threadName);
Thread thread = threadFactory.newThread(r);
try {
thread.setName(threadName);
thread.setDaemon(true);
} catch (AccessControlException ignore) {
// Unsupported on App Engine.
}
if (requestedRestart.getAndSet(false)) {
afterRestart();
}
return thread;
}
});
}
@Override
public void execute(Runnable runnable) {
// This gets called when the execute() method from Executor is directly invoked.
ensureRunning();
super.execute(runnable);
}
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
// This gets called by ScheduledThreadPoolExecutor before scheduling a Runnable.
ensureRunning();
return task;
}
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(
Callable<V> callable, RunnableScheduledFuture<V> task) {
// This gets called by ScheduledThreadPoolExecutor before scheduling a Callable.
ensureRunning();
return task;
}
@Override
protected void afterExecute(Runnable runnable, Throwable throwable) {
super.afterExecute(runnable, throwable);
if (throwable == null && runnable instanceof Future<?>) {
Future<?> future = (Future<?>) runnable;
try {
// Not all Futures will be done, e.g. when used with scheduledAtFixedRate
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
// Cancellation exceptions are okay, we expect them to happen sometimes
} catch (ExecutionException ee) {
throwable = ee.getCause();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (throwable == REVIVE_THREAD_EXCEPTION) {
// Re-throwing this exception will kill the thread and cause
// ScheduledThreadPoolExecutor to
// spawn a new thread.
throw (RuntimeException) throwable;
} else if (throwable != null) {
handleException(throwable);
}
}
/**
* Called when an exception occurs during execution of a Runnable/Callable. The default
* implementation does nothing.
*/
protected void handleException(Throwable throwable) {}
/** Called before the worker thread gets shutdown before a restart. */
protected void beforeRestart() {}
/** Called after the worker thread got recreated after a restart. */
protected void afterRestart() {}
private synchronized void ensureRunning() {
if (getCorePoolSize() == 0) {
setCorePoolSize(1);
schedulePeriodicShutdown();
}
}
private void schedulePeriodicShutdown() {
if (timeoutMs >= 0) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
schedule(
new Runnable() {
@Override
public void run() {
// We have to manually reschedule this task here as periodic tasks get
// cancelled after
// throwing exceptions.
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError1 =
RevivingScheduledExecutor.this.schedule(
this, timeoutMs, TimeUnit.MILLISECONDS);
requestedRestart.set(true);
beforeRestart();
throw REVIVE_THREAD_EXCEPTION;
}
},
initialDelayMs + timeoutMs,
TimeUnit.MILLISECONDS);
}
}
}