Repository: firebase/firebase-admin-java
Pull request #74 (2017-09-18 23:18:16)

Introducing the ThreadManager API
Adding the new `ThreadManager` interface which controls how an app obtains its thread pools and thread factories. Developers can specify their own implementations of this interface via `FirebaseOptions`. We provide 2 implementations (one for basic JVM and one for GAE), one of which will be used by default.

This PR also ensures:
1. Calling `app.delete()` terminates any and all threads started by the SDK.
2. When running in a regular JVM (non-GAE), we start all threads as daemons. This ensures thread clean up even if the user ignores to call `app.delete()`.
3. Database uses the `ThreadFactory` provided by `ThreadManager` to start all of its threads (event target, run loop, socket reader/writer etc.)
4. The SDK doesn't use `TaskExecutors` for any thing. However, we continue to keep it in the code base for backward compatibility reasons. 

hiranya911: Related to #55 and #73
hiranya911: I've been running some tests with this PR on App Engine, and I'll document my observations here.

### Manual Scaling

Works as usual. All async tasks (from auth, database and everything else) runs normally with the default `ThreadManager` chosen by the SDK. This is the preferred mode of deployment for accessing RTDB from App Engine environment.

### Automatic Scaling

Works as usual. All async tasks, except for the ones started by database, runs normally with the default `ThreadManager` chosen by the SDK. The database code requires long-lived threads, and hence cannot be used. The SDK will throw exceptions if attempted. But other components like auth can be used normally.

### Basic Scaling

This is similar to manual scaling. So everything, including database, will work as usual. However, the long-lived threads started by the database, and the proactive token refresher will prevent the instance from shutting down due to instance idle time (unless `FirebaseApp.delete()` is called). If this is important one must ensure that:

1. No database calls are made in the app, and
2. Proactive token refresher is disabled.

We will provide an API to achieve 2 in the future. In the meantime, one can use a custom executor (specified via `ThreadManager`). I had success with the following:

```
class MyExecutor extends ScheduledThreadPoolExecutor {
    public MyExecutor(ThreadFactory factory) {
        super(0, factory);
        setKeepAliveTime(60L, TimeUnit.SECONDS);
    }

    public <V> ScheduledFuture<V> schedule(Callable<V> command, long delay, TimeUnit unit) {
        if (delay > 0) {
            throw new UnsupportedOperationException();
        }
        return super.schedule(command, delay, unit);
    }

     public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
        if (delay > 0) {
            throw new UnsupportedOperationException();
        }
        return super.schedule(command, delay, unit);
    }
}
```

This essentially rejects all "scheduled" tasks, and only admits tasks for immediate execution (i.e. `delay == 0`). This will transparently fail the proactive token refresher, but execute everything else normally. 

I had my basic-scaled instance set to shutdown after 1 minute of idle time:

```
<basic-scaling>
  <max-instances>1</max-instances>
  <idle-timeout>1m</idle-timeout>
</basic-scaling>
```

With the above custom executor, I observed that my instance is shutting down after 2 minutes of idle time (1 extra minute due to the 60s keep alive time set on the executor).  So overall a successful test.
hiranya911: Made most of the suggested changes. A few more changes are on their way in #77. In particular some of the complexities you pointed out in the semantics of `ThreadManager` will get improved for the better.
hiranya911: Made the suggested changes. Over to @schmidt-sebastian for another round.

Patch:
diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java
index d2c97d2..91c47fc 100644
--- a/src/main/java/com/google/firebase/FirebaseApp.java
+++ b/src/main/java/com/google/firebase/FirebaseApp.java
@@ -32,12 +32,14 @@ import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.BaseEncoding;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
 import com.google.firebase.internal.FirebaseAppStore;
-import com.google.firebase.internal.FirebaseExecutors;
 import com.google.firebase.internal.FirebaseService;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 
+import com.google.firebase.tasks.Task;
+import com.google.firebase.tasks.Tasks;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -47,9 +49,12 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -84,9 +89,11 @@ public class FirebaseApp {
   private final String name;
   private final FirebaseOptions options;
   private final TokenRefresher tokenRefresher;
+  private final ThreadManager threadManager;
 
   private final AtomicBoolean deleted = new AtomicBoolean();
   private final Map<String, FirebaseService> services = new HashMap<>();
+  private final ListeningScheduledExecutorService executor;
 
   /**
    * Per application lock for synchronizing all internal FirebaseApp state changes.
@@ -99,6 +106,8 @@ public class FirebaseApp {
     this.name = name;
     this.options = checkNotNull(options);
     this.tokenRefresher = checkNotNull(factory).create(this);
+    this.threadManager = options.getThreadManager();
+    this.executor = this.threadManager.getListeningExecutor(this);
   }
 
   /** Returns a list of all FirebaseApps. */
@@ -242,7 +251,6 @@ public class FirebaseApp {
   /** Returns the unique name of this app. */
   @NonNull
   public String getName() {
-    checkNotDeleted();
     return name;
   }
 
@@ -316,6 +324,9 @@ public class FirebaseApp {
       }
       services.clear();
       tokenRefresher.cleanup();
+
+      // Clean up and terminate the thread pool
+      threadManager.releaseExecutor(this, executor);
     }
 
     synchronized (appsLock) {
@@ -332,6 +343,21 @@ public class FirebaseApp {
     checkState(!deleted.get(), "FirebaseApp was deleted %s", this);
   }
 
+  ThreadFactory getThreadFactory() {
+    return threadManager.getThreadFactory();
+  }
+
+  // TODO: Return an ApiFuture once Task API is fully removed.
+  <T> Task<T> submit(Callable<T> command) {
+    checkNotNull(command);
+    return Tasks.call(executor, command);
+  }
+
+  <T> ScheduledFuture<T> schedule(Callable<T> command, long delayMillis) {
+    checkNotNull(command);
+    return executor.schedule(command, delayMillis, TimeUnit.MILLISECONDS);
+  }
+
   boolean isDefaultApp() {
     return DEFAULT_APP_NAME.equals(getName());
   }
@@ -362,12 +388,14 @@ public class FirebaseApp {
    */
   static class TokenRefresher implements CredentialsChangedListener {
 
+    private final FirebaseApp firebaseApp;
     private final GoogleCredentials credentials;
-    private ScheduledFuture<Void> future;
+    private Future<Void> future;
     private boolean closed;
 
-    TokenRefresher(FirebaseApp app) {
-      this.credentials = app.getOptions().getCredentials();
+    TokenRefresher(FirebaseApp firebaseApp) {
+      this.firebaseApp = checkNotNull(firebaseApp);
+      this.credentials = firebaseApp.getOptions().getCredentials();
       this.credentials.addChangeListener(this);
     }
 
@@ -417,9 +445,7 @@ public class FirebaseApp {
     protected void scheduleNext(Callable<Void> task, long delayMillis) {
       logger.debug("Scheduling next token refresh in {} milliseconds", delayMillis);
       try {
-        future =
-            FirebaseExecutors.DEFAULT_SCHEDULED_EXECUTOR.schedule(
-                task, delayMillis, TimeUnit.MILLISECONDS);
+        future = firebaseApp.schedule(task, delayMillis);
       } catch (UnsupportedOperationException ignored) {
         // Cannot support task scheduling in the current runtime.
       }
diff --git a/src/main/java/com/google/firebase/FirebaseOptions.java b/src/main/java/com/google/firebase/FirebaseOptions.java
index 3cfb131..a485516 100644
--- a/src/main/java/com/google/firebase/FirebaseOptions.java
+++ b/src/main/java/com/google/firebase/FirebaseOptions.java
@@ -28,6 +28,7 @@ import com.google.firebase.auth.FirebaseCredential;
 import com.google.firebase.auth.FirebaseCredentials;
 import com.google.firebase.auth.internal.BaseCredential;
 import com.google.firebase.auth.internal.FirebaseCredentialsAdapter;
+import com.google.firebase.internal.FirebaseExecutors;
 import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.Nullable;
 
@@ -46,6 +47,7 @@ public final class FirebaseOptions {
   private final String projectId;
   private final HttpTransport httpTransport;
   private final JsonFactory jsonFactory;
+  private final ThreadManager threadManager;
 
   private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) {
     this.credentials = checkNotNull(builder.credentials,
@@ -59,6 +61,8 @@ public final class FirebaseOptions {
         "FirebaseOptions must be initialized with a non-null HttpTransport.");
     this.jsonFactory = checkNotNull(builder.jsonFactory,
         "FirebaseOptions must be initialized with a non-null JsonFactory.");
+    this.threadManager = checkNotNull(builder.threadManager,
+        "FirebaseOptions must be initialized with a non-null ThreadManager");
   }
 
   /**
@@ -118,7 +122,12 @@ public final class FirebaseOptions {
     return jsonFactory;
   }
 
-  /** 
+  @NonNull
+  ThreadManager getThreadManager() {
+    return threadManager;
+  }
+
+  /**
    * Builder for constructing {@link FirebaseOptions}. 
    */
   public static final class Builder {
@@ -130,6 +139,7 @@ public final class FirebaseOptions {
     private String projectId;
     private HttpTransport httpTransport = Utils.getDefaultTransport();
     private JsonFactory jsonFactory = Utils.getDefaultJsonFactory();
+    private ThreadManager threadManager = FirebaseExecutors.DEFAULT_THREAD_MANAGER;
 
     /** Constructs an empty builder. */
     public Builder() {}
@@ -148,6 +158,7 @@ public final class FirebaseOptions {
       projectId = options.projectId;
       httpTransport = options.httpTransport;
       jsonFactory = options.jsonFactory;
+      threadManager = options.threadManager;
     }
 
     /**
@@ -263,6 +274,18 @@ public final class FirebaseOptions {
       return this;
     }
 
+    /**
+     * Sets the <code>ThreadManager</code> used to initialize thread pools and thread factories
+     * for Firebase apps.
+     *
+     * @param threadManager A <code>ThreadManager</code> instance.
+     * @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
+     */
+    public Builder setThreadManager(ThreadManager threadManager) {
+      this.threadManager = threadManager;
+      return this;
+    }
+
     /**
      * Builds the {@link FirebaseOptions} instance from the previously set options.
      *
diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java
index 5931d24..65a8a10 100644
--- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java
+++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java
@@ -20,6 +20,13 @@ import com.google.auth.oauth2.GoogleCredentials;
 import com.google.firebase.internal.FirebaseService;
 import com.google.firebase.internal.NonNull;
 
+import com.google.firebase.tasks.Task;
+import com.google.firebase.tasks.Tasks;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+
 /**
  * Provides trampolines into package-private APIs used by components of Firebase. Intentionally
  * scarily-named to dissuade people from actually trying to use the class and to make it less likely
@@ -61,4 +68,12 @@ public final class ImplFirebaseTrampolines {
     app.addService(service);
     return service;
   }
+
+  public static ThreadFactory getThreadFactory(@NonNull FirebaseApp app) {
+    return app.getThreadFactory();
+  }
+
+  public static <T> Task<T> submitCallable(@NonNull FirebaseApp app, @NonNull Callable<T> command) {
+    return app.submit(command);
+  }
 }
diff --git a/src/main/java/com/google/firebase/ThreadManager.java b/src/main/java/com/google/firebase/ThreadManager.java
new file mode 100644
index 0000000..0c65ad1
--- /dev/null
+++ b/src/main/java/com/google/firebase/ThreadManager.java
@@ -0,0 +1,77 @@
+/*
+ * 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;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.firebase.internal.NonNull;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * An interface that controls the thread pools and thread factories used by the Admin SDK. Each
+ * instance of {@link FirebaseApp} uses an implementation of this interface to create and manage
+ * threads. Multiple app instances may use the same <code>ThreadManager</code> instance.
+ * Methods in this interface may get invoked multiple times by the same
+ * app, during its lifetime. Apps may also invoke methods of this interface concurrently, and
+ * therefore implementations should provide any synchronization necessary.
+ */
+public abstract class ThreadManager {
+
+  @NonNull
+  final ListeningScheduledExecutorService getListeningExecutor(@NonNull FirebaseApp app) {
+    ScheduledExecutorService executor = getExecutor(app);
+    checkNotNull(executor, "ScheduledExecutorService must not be null");
+    return MoreExecutors.listeningDecorator(executor);
+  }
+
+  /**
+   * Returns the main thread pool for an app. Implementations may return the same instance of
+   * <code>ScheduledExecutorService</code> for multiple apps. The returned thread pool is used by
+   * all components of an app except for the Realtime Database. Database has far stricter and
+   * complicated threading requirements, and thus initializes its own threads using the
+   * factory returned by {@link ThreadManager#getThreadFactory()}.
+   *
+   * @param app A {@link FirebaseApp} instance.
+   * @return A non-null {@link ScheduledExecutorService} instance.
+   */
+  @NonNull
+  protected abstract ScheduledExecutorService getExecutor(@NonNull FirebaseApp app);
+
+  /**
+   * Cleans up the thread pool associated with an app. This method is invoked when an
+   * app is deleted.
+   *
+   * @param app A {@link FirebaseApp} instance.
+   */
+  protected abstract void releaseExecutor(
+      @NonNull FirebaseApp app, @NonNull ScheduledExecutorService executor);
+
+  /**
+   * Returns the <code>ThreadFactory</code> to be used for creating any additional threads
+   * required by the SDK. This is used mainly to create the long-lived worker threads for
+   * the Realtime Database client.
+   *
+   * @return A non-null <code>ThreadFactory</code>.
+   */
+  @NonNull
+  protected abstract ThreadFactory getThreadFactory();
+
+}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index e4afeae..7475888 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -35,11 +35,8 @@ import com.google.firebase.auth.UserRecord.UpdateRequest;
 import com.google.firebase.auth.internal.FirebaseTokenFactory;
 import com.google.firebase.auth.internal.FirebaseTokenVerifier;
 import com.google.firebase.internal.FirebaseService;
-import com.google.firebase.internal.NonNull;
 import com.google.firebase.internal.TaskToApiFuture;
-import com.google.firebase.tasks.Continuation;
 import com.google.firebase.tasks.Task;
-import com.google.firebase.tasks.Tasks;
 
 import java.util.Map;
 import java.util.concurrent.Callable;
@@ -55,10 +52,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
  */
 public class FirebaseAuth {
 
-  private final GoogleCredentials credentials;
-  private final String projectId;
   private final GooglePublicKeysManager googlePublicKeysManager;
   private final Clock clock;
+
+  private final FirebaseApp firebaseApp;
+  private final GoogleCredentials credentials;
+  private final String projectId;
   private final JsonFactory jsonFactory;
   private final FirebaseUserManager userManager;
   private final AtomicBoolean destroyed;
@@ -75,10 +74,11 @@ public class FirebaseAuth {
   @VisibleForTesting
   FirebaseAuth(
       FirebaseApp firebaseApp, GooglePublicKeysManager googlePublicKeysManager, Clock clock) {
+    this.firebaseApp = checkNotNull(firebaseApp);
+    this.googlePublicKeysManager = checkNotNull(googlePublicKeysManager);
+    this.clock = checkNotNull(clock);
     this.credentials = ImplFirebaseTrampolines.getCredentials(firebaseApp);
     this.projectId = ImplFirebaseTrampolines.getProjectId(firebaseApp);
-    this.googlePublicKeysManager = googlePublicKeysManager;
-    this.clock = clock;
     this.jsonFactory = firebaseApp.getOptions().getJsonFactory();
     this.userManager = new FirebaseUserManager(jsonFactory,
         firebaseApp.getOptions().getHttpTransport(), this.credentials);
@@ -143,7 +143,7 @@ public class FirebaseAuth {
             + "createCustomToken()");
 
     final ServiceAccountCredentials serviceAccount = (ServiceAccountCredentials) credentials;
-    return Tasks.call(new Callable<String>() {
+    return call(new Callable<String>() {
       @Override
       public String call() throws Exception {
         FirebaseTokenFactory tokenFactory = FirebaseTokenFactory.getInstance();
@@ -199,25 +199,23 @@ public class FirebaseAuth {
     checkNotDestroyed();
     checkState(!Strings.isNullOrEmpty(projectId),
         "Must initialize FirebaseApp with a project ID to call verifyIdToken()");
-    return Tasks.forResult(projectId)
-        .continueWith(
-            new Continuation<String, FirebaseToken>() {
-              @Override
-              public FirebaseToken then(@NonNull Task<String> task) throws Exception {
-                FirebaseTokenVerifier firebaseTokenVerifier =
-                    new FirebaseTokenVerifier.Builder()
-                        .setProjectId(task.getResult())
-                        .setPublicKeysManager(googlePublicKeysManager)
-                        .setClock(clock)
-                        .build();
-                FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, token);
-
-                // This will throw a FirebaseAuthException with details on how the token is invalid.
-                firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
-
-                return firebaseToken;
-              }
-            });
+    return call(new Callable<FirebaseToken>() {
+      @Override
+      public FirebaseToken call() throws Exception {
+        FirebaseTokenVerifier firebaseTokenVerifier =
+            new FirebaseTokenVerifier.Builder()
+                .setProjectId(projectId)
+                .setPublicKeysManager(googlePublicKeysManager)
+                .setClock(clock)
+                .build();
+        FirebaseToken firebaseToken = FirebaseToken.parse(jsonFactory, token);
+
+        // This will throw a FirebaseAuthException with details on how the token is invalid.
+        firebaseTokenVerifier.verifyTokenAndSignature(firebaseToken.getToken());
+
+        return firebaseToken;
+      }
+    });
   }
 
   /**
@@ -257,7 +255,7 @@ public class FirebaseAuth {
   public Task<UserRecord> getUser(final String uid) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
-    return Tasks.call(new Callable<UserRecord>() {
+    return call(new Callable<UserRecord>() {
       @Override
       public UserRecord call() throws Exception {
         return userManager.getUserById(uid);
@@ -291,7 +289,7 @@ public class FirebaseAuth {
   public Task<UserRecord> getUserByEmail(final String email) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty");
-    return Tasks.call(new Callable<UserRecord>() {
+    return call(new Callable<UserRecord>() {
       @Override
       public UserRecord call() throws Exception {
         return userManager.getUserByEmail(email);
@@ -325,7 +323,7 @@ public class FirebaseAuth {
   public Task<UserRecord> getUserByPhoneNumber(final String phoneNumber) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty");
-    return Tasks.call(new Callable<UserRecord>() {
+    return call(new Callable<UserRecord>() {
       @Override
       public UserRecord call() throws Exception {
         return userManager.getUserByPhoneNumber(phoneNumber);
@@ -359,7 +357,7 @@ public class FirebaseAuth {
   public Task<UserRecord> createUser(final CreateRequest request) {
     checkNotDestroyed();
     checkNotNull(request, "create request must not be null");
-    return Tasks.call(new Callable<UserRecord>() {
+    return call(new Callable<UserRecord>() {
       @Override
       public UserRecord call() throws Exception {
         String uid = userManager.createUser(request);
@@ -395,7 +393,7 @@ public class FirebaseAuth {
   public Task<UserRecord> updateUser(final UpdateRequest request) {
     checkNotDestroyed();
     checkNotNull(request, "update request must not be null");
-    return Tasks.call(new Callable<UserRecord>() {
+    return call(new Callable<UserRecord>() {
       @Override
       public UserRecord call() throws Exception {
         userManager.updateUser(request);
@@ -431,7 +429,7 @@ public class FirebaseAuth {
   public Task<Void> deleteUser(final String uid) {
     checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
-    return Tasks.call(new Callable<Void>() {
+    return call(new Callable<Void>() {
       @Override
       public Void call() throws Exception {
         userManager.deleteUser(uid);
@@ -466,6 +464,10 @@ public class FirebaseAuth {
     return new TaskToApiFuture<>(deleteUser(uid));
   }
 
+  private <T> Task<T> call(Callable<T> command) {
+    return ImplFirebaseTrampolines.submitCallable(firebaseApp, command);
+  }
+
   private static final String SERVICE_ID = FirebaseAuth.class.getName();
 
   private static class FirebaseAuthService extends FirebaseService<FirebaseAuth> {
diff --git a/src/main/java/com/google/firebase/auth/internal/BaseCredential.java b/src/main/java/com/google/firebase/auth/internal/BaseCredential.java
index c5b5be6..2e717ad 100644
--- a/src/main/java/com/google/firebase/auth/internal/BaseCredential.java
+++ b/src/main/java/com/google/firebase/auth/internal/BaseCredential.java
@@ -25,8 +25,9 @@ import com.google.firebase.auth.FirebaseCredential;
 import com.google.firebase.auth.GoogleOAuthAccessToken;
 import com.google.firebase.tasks.Task;
 import com.google.firebase.tasks.Tasks;
+
+import java.io.IOException;
 import java.util.List;
-import java.util.concurrent.Callable;
 
 /**
  * Internal base class for built-in FirebaseCredential implementations.
@@ -59,14 +60,14 @@ public abstract class BaseCredential implements FirebaseCredential {
 
   @Override
   public Task<GoogleOAuthAccessToken> getAccessToken() {
-    return Tasks.call(new Callable<GoogleOAuthAccessToken>() {
-      @Override
-      public GoogleOAuthAccessToken call() throws Exception {
-        AccessToken accessToken = googleCredentials.refreshAccessToken();
-        return new GoogleOAuthAccessToken(accessToken.getTokenValue(),
-            accessToken.getExpirationTime().getTime());
-      }
-    });
+    try {
+      AccessToken accessToken = googleCredentials.refreshAccessToken();
+      GoogleOAuthAccessToken googleToken = new GoogleOAuthAccessToken(accessToken.getTokenValue(),
+          accessToken.getExpirationTime().getTime());
+      return Tasks.forResult(googleToken);
+    } catch (Exception e) {
+      return Tasks.forException(e);
+    }
   }
 
 }
diff --git a/src/main/java/com/google/firebase/database/connection/ConnectionContext.java b/src/main/java/com/google/firebase/database/connection/ConnectionContext.java
index 4bde8c5..eb767d0 100644
--- a/src/main/java/com/google/firebase/database/connection/ConnectionContext.java
+++ b/src/main/java/com/google/firebase/database/connection/ConnectionContext.java
@@ -17,6 +17,7 @@
 package com.google.firebase.database.connection;
 
 import com.google.firebase.database.logging.Logger;
+import com.google.firebase.database.tubesock.ThreadConfig;
 
 import java.util.concurrent.ScheduledExecutorService;
 
@@ -28,6 +29,7 @@ public class ConnectionContext {
   private final boolean persistenceEnabled;
   private final String clientSdkVersion;
   private final String userAgent;
+  private final ThreadConfig threadConfig;
 
   public ConnectionContext(
       Logger logger,
@@ -35,13 +37,15 @@ public class ConnectionContext {
       ScheduledExecutorService executorService,
       boolean persistenceEnabled,
       String clientSdkVersion,
-      String userAgent) {
+      String userAgent,
+      ThreadConfig threadConfig) {
     this.logger = logger;
     this.authTokenProvider = authTokenProvider;
     this.executorService = executorService;
     this.persistenceEnabled = persistenceEnabled;
     this.clientSdkVersion = clientSdkVersion;
     this.userAgent = userAgent;
+    this.threadConfig = threadConfig;
   }
 
   public Logger getLogger() {
@@ -67,4 +71,8 @@ public class ConnectionContext {
   public String getUserAgent() {
     return this.userAgent;
   }
+
+  public ThreadConfig getThreadConfig() {
+    return threadConfig;
+  }
 }
diff --git a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
index 12afdfd..ea4248e 100644
--- a/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
+++ b/src/main/java/com/google/firebase/database/connection/WebsocketConnection.java
@@ -89,9 +89,9 @@ class WebsocketConnection {
             host, hostInfo.isSecure(), hostInfo.getNamespace(), optLastSessionId);
     Map<String, String> extraHeaders = new HashMap<>();
     extraHeaders.put("User-Agent", this.connectionContext.getUserAgent());
-    WebSocket ws = new WebSocket(uri, /*protocol=*/ null, extraHeaders);
-    WSClientTubesock client = new WSClientTubesock(ws);
-    return client;
+    WebSocket ws = new WebSocket(uri, /*protocol=*/ null, extraHeaders,
+        connectionContext.getThreadConfig());
+    return new WSClientTubesock(ws);
   }
 
   public void open() {
diff --git a/src/main/java/com/google/firebase/database/core/Context.java b/src/main/java/com/google/firebase/database/core/Context.java
index d691fac..f5b8a7f 100644
--- a/src/main/java/com/google/firebase/database/core/Context.java
+++ b/src/main/java/com/google/firebase/database/core/Context.java
@@ -17,6 +17,7 @@
 package com.google.firebase.database.core;
 
 import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.database.DatabaseException;
 import com.google.firebase.database.FirebaseDatabase;
 import com.google.firebase.database.connection.ConnectionAuthTokenProvider;
@@ -27,6 +28,7 @@ import com.google.firebase.database.core.persistence.NoopPersistenceManager;
 import com.google.firebase.database.core.persistence.PersistenceManager;
 import com.google.firebase.database.logging.LogWrapper;
 import com.google.firebase.database.logging.Logger;
+import com.google.firebase.database.tubesock.ThreadConfig;
 import com.google.firebase.database.utilities.DefaultRunLoop;
 
 import java.util.List;
@@ -78,9 +80,7 @@ public class Context {
   private Platform getPlatform() {
     if (platform == null) {
       if (GaePlatform.isActive()) {
-        GaePlatform gaePlatform = new GaePlatform(firebaseApp);
-        gaePlatform.initialize();
-        platform = gaePlatform;
+        platform = new GaePlatform(firebaseApp);
       } else {
         platform = new JvmPlatform(firebaseApp);
       }
@@ -153,6 +153,11 @@ public class Context {
     return new LogWrapper(logger, component, prefix);
   }
 
+  private ThreadConfig getThreadConfig() {
+    return new ThreadConfig(ImplFirebaseTrampolines.getThreadFactory(firebaseApp),
+        getPlatform().getThreadInitializer());
+  }
+
   public ConnectionContext getConnectionContext() {
     return new ConnectionContext(
         this.logger,
@@ -160,7 +165,8 @@ public class Context {
         this.getExecutorService(),
         this.isPersistenceEnabled(),
         FirebaseDatabase.getSdkVersion(),
-        this.getUserAgent());
+        this.getUserAgent(),
+        this.getThreadConfig());
   }
 
   PersistenceManager getPersistenceManager(String firebaseId) {
diff --git a/src/main/java/com/google/firebase/database/core/GaePlatform.java b/src/main/java/com/google/firebase/database/core/GaePlatform.java
index 4438f54..b7698f3 100644
--- a/src/main/java/com/google/firebase/database/core/GaePlatform.java
+++ b/src/main/java/com/google/firebase/database/core/GaePlatform.java
@@ -16,9 +16,8 @@
 
 package com.google.firebase.database.core;
 
-import static com.google.common.base.Preconditions.checkState;
-
 import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.database.FirebaseDatabase;
 import com.google.firebase.database.connection.ConnectionContext;
 import com.google.firebase.database.connection.HostInfo;
@@ -28,7 +27,6 @@ import com.google.firebase.database.core.persistence.PersistenceManager;
 import com.google.firebase.database.logging.DefaultLogger;
 import com.google.firebase.database.logging.LogWrapper;
 import com.google.firebase.database.logging.Logger;
-import com.google.firebase.database.tubesock.WebSocket;
 import com.google.firebase.database.utilities.DefaultRunLoop;
 import com.google.firebase.internal.GaeThreadFactory;
 import com.google.firebase.internal.RevivingScheduledExecutor;
@@ -44,10 +42,8 @@ import java.util.concurrent.ThreadFactory;
  */
 class GaePlatform implements Platform {
 
-  private static final String TAG = "GaePlatform";
   private static final String PROCESS_PLATFORM = "AppEngine";
   private final FirebaseApp firebaseApp;
-  ThreadFactory threadFactoryInstance;
 
   public GaePlatform(FirebaseApp firebaseApp) {
     this.firebaseApp = firebaseApp;
@@ -63,21 +59,7 @@ class GaePlatform implements Platform {
   }
 
   private ThreadFactory getGaeThreadFactory() {
-    GaeThreadFactory threadFactory = GaeThreadFactory.getInstance();
-    checkState(threadFactory.isUsingBackgroundThreads(),
-        "Failed to initialize a GAE background thread factory");
-    return threadFactory;
-  }
-
-  public void initialize() {
-    WebSocket.setThreadFactory(
-        getGaeThreadFactory(),
-        new com.google.firebase.database.tubesock.ThreadInitializer() {
-          @Override
-          public void setName(Thread thread, String s) {
-            // Unsupported by GAE
-          }
-        });
+    return ImplFirebaseTrampolines.getThreadFactory(firebaseApp);
   }
 
   @Override
@@ -126,4 +108,24 @@ class GaePlatform implements Platform {
   public PersistenceManager createPersistenceManager(Context ctx, String namespace) {
     return null;
   }
+
+  @Override
+  public ThreadInitializer getThreadInitializer() {
+    return new ThreadInitializer() {
+      @Override
+      public void setName(Thread t, String name) {
+        // Unsupported by GAE
+      }
+
+      @Override
+      public void setDaemon(Thread t, boolean isDaemon) {
+        // Unsupported by GAE
+      }
+
+      @Override
+      public void setUncaughtExceptionHandler(Thread t, Thread.UncaughtExceptionHandler handler) {
+        // Unsupported by GAE
+      }
+    };
+  }
 }
diff --git a/src/main/java/com/google/firebase/database/core/JvmPlatform.java b/src/main/java/com/google/firebase/database/core/JvmPlatform.java
index f6985fd..30a2e25 100644
--- a/src/main/java/com/google/firebase/database/core/JvmPlatform.java
+++ b/src/main/java/com/google/firebase/database/core/JvmPlatform.java
@@ -17,6 +17,7 @@
 package com.google.firebase.database.core;
 
 import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
 import com.google.firebase.database.FirebaseDatabase;
 import com.google.firebase.database.connection.ConnectionContext;
 import com.google.firebase.database.connection.HostInfo;
@@ -29,8 +30,8 @@ import com.google.firebase.database.logging.Logger;
 import com.google.firebase.database.utilities.DefaultRunLoop;
 
 import java.util.List;
-import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
 
 class JvmPlatform implements Platform {
 
@@ -49,14 +50,15 @@ class JvmPlatform implements Platform {
 
   @Override
   public EventTarget newEventTarget(Context ctx) {
-    return new ThreadPoolEventTarget(
-        Executors.defaultThreadFactory(), ThreadInitializer.defaultInstance);
+    ThreadFactory threadFactory = ImplFirebaseTrampolines.getThreadFactory(firebaseApp);
+    return new ThreadPoolEventTarget(threadFactory, ThreadInitializer.defaultInstance);
   }
 
   @Override
   public RunLoop newRunLoop(final Context context) {
     final LogWrapper logger = context.getLogger(RunLoop.class);
-    return new DefaultRunLoop() {
+    ThreadFactory threadFactory = ImplFirebaseTrampolines.getThreadFactory(firebaseApp);
+    return new DefaultRunLoop(threadFactory) {
       @Override
       public void handleException(Throwable e) {
         logger.error(DefaultRunLoop.messageForException(e), e);
@@ -92,4 +94,9 @@ class JvmPlatform implements Platform {
   public PersistenceManager createPersistenceManager(Context ctx, String namespace) {
     return null;
   }
+
+  @Override
+  public ThreadInitializer getThreadInitializer() {
+    return ThreadInitializer.defaultInstance;
+  }
 }
diff --git a/src/main/java/com/google/firebase/database/core/Platform.java b/src/main/java/com/google/firebase/database/core/Platform.java
index 320a851..5e701c4 100644
--- a/src/main/java/com/google/firebase/database/core/Platform.java
+++ b/src/main/java/com/google/firebase/database/core/Platform.java
@@ -48,4 +48,6 @@ public interface Platform {
   String getPlatformVersion();
 
   PersistenceManager createPersistenceManager(Context ctx, String firebaseId);
+
+  ThreadInitializer getThreadInitializer();
 }
diff --git a/src/main/java/com/google/firebase/database/tubesock/ThreadConfig.java b/src/main/java/com/google/firebase/database/tubesock/ThreadConfig.java
new file mode 100644
index 0000000..4839a22
--- /dev/null
+++ b/src/main/java/com/google/firebase/database/tubesock/ThreadConfig.java
@@ -0,0 +1,62 @@
+/*
+ * 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.database.tubesock;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.firebase.database.core.ThreadInitializer;
+
+import com.google.firebase.internal.NonNull;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Configuration that governs how the websocket connections spawn and initialize threads
+ * for reading and writing data.
+ */
+public final class ThreadConfig {
+
+  private final ThreadFactory threadFactory;
+  private final ThreadInitializer threadInitializer;
+
+  public ThreadConfig(ThreadFactory threadFactory, ThreadInitializer threadInitializer) {
+    this.threadFactory = checkNotNull(threadFactory);
+    this.threadInitializer = checkNotNull(threadInitializer);
+  }
+
+  /**
+   * The <code>ThreadFactory</code> implementation to be used for creating (spawning) new
+   * threads.
+   *
+   * @return a non-null <code>ThreadFactory</code> instance.
+   */
+  @NonNull
+  ThreadFactory getThreadFactory() {
+    return threadFactory;
+  }
+
+  /**
+   * The {@link ThreadInitializer} implementation to be used for further configuring the threads
+   * created by the <code>ThreadFactory</code>. In particular, this instance is used to rename
+   * the created threads, and turn them into daemons if necessary.
+   *
+   * @return a non-null {@link ThreadInitializer} instance.
+   */
+  @NonNull
+  ThreadInitializer getInitializer() {
+    return threadInitializer;
+  }
+}
diff --git a/src/main/java/com/google/firebase/database/tubesock/ThreadInitializer.java b/src/main/java/com/google/firebase/database/tubesock/ThreadInitializer.java
deleted file mode 100644
index d344ea4..0000000
--- a/src/main/java/com/google/firebase/database/tubesock/ThreadInitializer.java
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * 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.database.tubesock;
-
-public interface ThreadInitializer {
-
-  void setName(Thread t, String name);
-}
diff --git a/src/main/java/com/google/firebase/database/tubesock/WebSocket.java b/src/main/java/com/google/firebase/database/tubesock/WebSocket.java
index d61b6dd..3d2e28c 100644
--- a/src/main/java/com/google/firebase/database/tubesock/WebSocket.java
+++ b/src/main/java/com/google/firebase/database/tubesock/WebSocket.java
@@ -16,8 +16,11 @@
 
 package com.google.firebase.database.tubesock;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
 
+import com.google.firebase.database.core.ThreadInitializer;
+
 import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -29,7 +32,6 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.net.SocketFactory;
@@ -44,6 +46,9 @@ import javax.net.ssl.SSLSocketFactory;
  */
 public class WebSocket {
 
+  private static final ThreadConfig DEFAULT_THREAD_CONFIG = new ThreadConfig(
+      Executors.defaultThreadFactory(), ThreadInitializer.defaultInstance);
+
   static final byte OPCODE_NONE = 0x0;
   static final byte OPCODE_TEXT = 0x1;
   static final byte OPCODE_BINARY = 0x2;
@@ -53,15 +58,9 @@ public class WebSocket {
   private static final String THREAD_BASE_NAME = "TubeSock";
   private static final AtomicInteger clientCount = new AtomicInteger(0);
   private static final Charset UTF8 = Charset.forName("UTF-8");
-  private static ThreadFactory threadFactory = Executors.defaultThreadFactory();
-  private static ThreadInitializer intializer =
-      new ThreadInitializer() {
-        @Override
-        public void setName(Thread t, String name) {
-          t.setName(name);
-        }
-      };
+
   private final URI url;
+  private final ThreadConfig threadConfig;
   private final WebSocketReceiver receiver;
   private final WebSocketWriter writer;
   private final WebSocketHandshake handshake;
@@ -76,18 +75,8 @@ public class WebSocket {
    *
    * @param url The URL of a websocket server
    */
-  public WebSocket(URI url) {
-    this(url, null);
-  }
-
-  /**
-   * Create a websocket to connect to a given server. Include protocol in websocket handshake
-   *
-   * @param url The URL of a websocket server
-   * @param protocol The protocol to include in the handshake. If null, it will be omitted
-   */
-  public WebSocket(URI url, String protocol) {
-    this(url, protocol, null);
+  WebSocket(URI url) {
+    this(url, null, null, DEFAULT_THREAD_CONFIG);
   }
 
   /**
@@ -99,26 +88,17 @@ public class WebSocket {
    * @param protocol The protocol to include in the handshake. If null, it will be omitted
    * @param extraHeaders Any extra HTTP headers to be included with the initial request. Pass null
    *     if not extra headers are requested
+   * @param threadConfig A non-null ThreadConfig for websocket connections.
    */
-  public WebSocket(URI url, String protocol, Map<String, String> extraHeaders) {
+  public WebSocket(URI url, String protocol, Map<String, String> extraHeaders,
+                   ThreadConfig threadConfig) {
     this.url = url;
     handshake = new WebSocketHandshake(url, protocol, extraHeaders);
     receiver = new WebSocketReceiver(this);
-    writer = new WebSocketWriter(this, THREAD_BASE_NAME, clientId);
-  }
-
-  static ThreadFactory getThreadFactory() {
-    return threadFactory;
-  }
-
-  static ThreadInitializer getIntializer() {
-    return intializer;
+    writer = new WebSocketWriter(this, threadConfig, THREAD_BASE_NAME, clientId);
+    this.threadConfig = checkNotNull(threadConfig);
   }
 
-  public static void setThreadFactory(ThreadFactory threadFactory, ThreadInitializer intializer) {
-    WebSocket.threadFactory = threadFactory;
-    WebSocket.intializer = intializer;
-  }
 
   WebSocketEventHandler getEventHandler() {
     return this.eventHandler;
@@ -150,7 +130,7 @@ public class WebSocket {
   private synchronized void start() {
     checkState(innerThread == null, "Inner thread already started");
     innerThread =
-        getThreadFactory()
+        threadConfig.getThreadFactory()
             .newThread(
                 new Runnable() {
                   @Override
@@ -158,7 +138,7 @@ public class WebSocket {
                     runReader();
                   }
                 });
-    getIntializer().setName(innerThread, THREAD_BASE_NAME + "Reader-" + clientId);
+    threadConfig.getInitializer().setName(innerThread, THREAD_BASE_NAME + "Reader-" + clientId);
     innerThread.start();
   }
 
diff --git a/src/main/java/com/google/firebase/database/tubesock/WebSocketWriter.java b/src/main/java/com/google/firebase/database/tubesock/WebSocketWriter.java
index 816a075..c4477e6 100644
--- a/src/main/java/com/google/firebase/database/tubesock/WebSocketWriter.java
+++ b/src/main/java/com/google/firebase/database/tubesock/WebSocketWriter.java
@@ -33,6 +33,8 @@ import java.util.concurrent.LinkedBlockingQueue;
  */
 class WebSocketWriter {
 
+  private final WebSocket websocket;
+  private final ThreadConfig threadConfig;
   private final Random random = new Random();
   private final String threadName;
 
@@ -40,11 +42,12 @@ class WebSocketWriter {
   private BlockingQueue<ByteBuffer> pendingBuffers;
   private volatile boolean stop = false;
   private boolean closeSent = false;
-  private WebSocket websocket;
   private WritableByteChannel channel;
 
-  WebSocketWriter(WebSocket websocket, String threadBaseName, int clientId) {
+  WebSocketWriter(WebSocket websocket, ThreadConfig threadConfig, String threadBaseName,
+                  int clientId) {
     this.websocket = websocket;
+    this.threadConfig = threadConfig;
     this.threadName = threadBaseName + "Writer-" + clientId;
     pendingBuffers = new LinkedBlockingQueue<>();
   }
@@ -163,7 +166,7 @@ class WebSocketWriter {
   synchronized void start() {
     checkState(innerThread == null, "Inner thread already started");
     innerThread =
-        WebSocket.getThreadFactory()
+        threadConfig.getThreadFactory()
             .newThread(
                 new Runnable() {
                   @Override
@@ -171,7 +174,7 @@ class WebSocketWriter {
                     runWriter();
                   }
                 });
-    WebSocket.getIntializer().setName(innerThread, threadName);
+    threadConfig.getInitializer().setName(innerThread, threadName);
     innerThread.start();
   }
 
diff --git a/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java b/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java
index fd61d7b..be1182d 100644
--- a/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java
+++ b/src/main/java/com/google/firebase/database/utilities/DefaultRunLoop.java
@@ -38,8 +38,8 @@ public abstract class DefaultRunLoop implements RunLoop {
   private UncaughtExceptionHandler exceptionHandler;
 
   /** Creates a DefaultRunLoop that does not periodically restart its threads. */
-  public DefaultRunLoop() {
-    this(Executors.defaultThreadFactory(), false, null);
+  public DefaultRunLoop(ThreadFactory threadFactory) {
+    this(threadFactory, false, null);
   }
 
   /**
diff --git a/src/main/java/com/google/firebase/internal/FirebaseExecutors.java b/src/main/java/com/google/firebase/internal/FirebaseExecutors.java
index a1dc7c4..1044a38 100644
--- a/src/main/java/com/google/firebase/internal/FirebaseExecutors.java
+++ b/src/main/java/com/google/firebase/internal/FirebaseExecutors.java
@@ -16,20 +16,127 @@
 
 package com.google.firebase.internal;
 
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.ThreadManager;
+
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
 
-/** Default executors used for internal Firebase threads. */
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Default ThreadManager implementations used by the Admin SDK. */
 public class FirebaseExecutors {
 
-  public static final ScheduledExecutorService DEFAULT_SCHEDULED_EXECUTOR;
+  private static final Logger logger = LoggerFactory.getLogger(FirebaseExecutors.class);
+
+  public static final ThreadManager DEFAULT_THREAD_MANAGER;
 
   static {
     if (GaeThreadFactory.isAvailable()) {
-      DEFAULT_SCHEDULED_EXECUTOR = GaeThreadFactory.DEFAULT_EXECUTOR;
+      DEFAULT_THREAD_MANAGER = new GaeThreadManager();
     } else {
-      DEFAULT_SCHEDULED_EXECUTOR =
-          Executors.newSingleThreadScheduledExecutor(Executors.defaultThreadFactory());
+      DEFAULT_THREAD_MANAGER = new DefaultThreadManager();
+    }
+  }
+
+  /**
+   * An abstract ThreadManager implementation that uses the same executor service
+   * across all active apps. The executor service is initialized when the first app is initialized,
+   * and terminated when the last app is deleted. This class is thread safe.
+   */
+  abstract static class GlobalThreadManager extends ThreadManager {
+
+    private final Object lock = new Object();
+    private final Set<String> apps = new HashSet<>();
+
+    private ScheduledExecutorService executorService;
+
+    @Override
+    protected ScheduledExecutorService getExecutor(FirebaseApp app) {
+      synchronized (lock) {
+        if (executorService == null) {
+          executorService = doInit();
+        }
+        apps.add(app.getName());
+        return executorService;
+      }
+    }
+
+    @Override
+    protected void releaseExecutor(FirebaseApp app, ScheduledExecutorService executor) {
+      synchronized (lock) {
+        if (apps.remove(app.getName()) && apps.isEmpty()) {
+          doCleanup(executorService);
+          executorService = null;
+        }
+      }
+    }
+
+    /**
+     * Initializes the executor service. Called when the first application is initialized.
+     */
+    protected abstract ScheduledExecutorService doInit();
+
+    /**
+     * Cleans up the executor service. Called when the last application is deleted.
+     */
+    protected abstract void doCleanup(ScheduledExecutorService executorService);
+  }
+
+  private static class DefaultThreadManager extends GlobalThreadManager {
+
+    @Override
+    protected ScheduledExecutorService doInit() {
+      int cores = Runtime.getRuntime().availableProcessors();
+      // Create threads as daemons to ensure JVM exit when all foreground jobs are complete.
+      ThreadFactory threadFactory = new ThreadFactoryBuilder()
+          .setNameFormat("firebase-default-%d")
+          .setDaemon(true)
+          .setThreadFactory(getThreadFactory())
+          .build();
+      logger.debug("Initializing default executor with {} max threads", cores);
+      return Executors.newScheduledThreadPool(cores, threadFactory);
+    }
+
+    @Override
+    protected void doCleanup(ScheduledExecutorService executorService) {
+      logger.debug("Shutting down default executor");
+      executorService.shutdownNow();
+    }
+
+    @Override
+    protected ThreadFactory getThreadFactory() {
+      return Executors.defaultThreadFactory();
+    }
+  }
+
+  private static class GaeThreadManager extends GlobalThreadManager {
+
+    @Override
+    protected ScheduledExecutorService doInit() {
+      return new GaeScheduledExecutorService("gae-firebase-default");
+    }
+
+    @Override
+    protected void doCleanup(ScheduledExecutorService executorService) {
+      executorService.shutdownNow();
+    }
+
+    @Override
+
+    protected ThreadFactory getThreadFactory() {
+      GaeThreadFactory threadFactory = GaeThreadFactory.getInstance();
+      checkState(threadFactory.isUsingBackgroundThreads(),
+          "Failed to initialize a GAE background thread factory. Background thread support "
+              + "is required to access the Realtime database from App Engine environment.");
+      return threadFactory;
     }
   }
 }
diff --git a/src/main/java/com/google/firebase/internal/GaeScheduledExecutorService.java b/src/main/java/com/google/firebase/internal/GaeScheduledExecutorService.java
index ab47fab..3be11e9 100644
--- a/src/main/java/com/google/firebase/internal/GaeScheduledExecutorService.java
+++ b/src/main/java/com/google/firebase/internal/GaeScheduledExecutorService.java
@@ -17,8 +17,11 @@
 package com.google.firebase.internal;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.Callable;
@@ -42,10 +45,11 @@ import java.util.concurrent.atomic.AtomicReference;
  * regardless of the background threads support. This implementation is also lazy loaded to prevent
  * unnecessary RPC calls to the GAE backend.
  */
-public class GaeScheduledExecutorService implements ScheduledExecutorService {
+class GaeScheduledExecutorService implements ScheduledExecutorService {
 
   private final AtomicReference<ExecutorWrapper> executor = new AtomicReference<>();
   private final String threadName;
+  private boolean shutdown;
 
   GaeScheduledExecutorService(String threadName) {
     checkArgument(!Strings.isNullOrEmpty(threadName));
@@ -56,6 +60,7 @@ public class GaeScheduledExecutorService implements ScheduledExecutorService {
     ExecutorWrapper wrapper = executor.get();
     if (wrapper == null) {
       synchronized (executor) {
+        checkState(!shutdown);
         wrapper = executor.get();
         if (wrapper == null) {
           wrapper = new ExecutorWrapper(threadName);
@@ -148,12 +153,28 @@ public class GaeScheduledExecutorService implements ScheduledExecutorService {
 
   @Override
   public void shutdown() {
-    ensureExecutorService().shutdown();
+    synchronized (executor) {
+      ExecutorWrapper wrapper = executor.get();
+      if (wrapper != null && !shutdown) {
+        wrapper.getExecutorService().shutdown();
+      }
+      shutdown = true;
+    }
   }
 
   @Override
   public List<Runnable> shutdownNow() {
-    return ensureExecutorService().shutdownNow();
+    synchronized (executor) {
+      ExecutorWrapper wrapper = executor.get();
+      List<Runnable> result;
+      if (wrapper != null && !shutdown) {
+        result = wrapper.getExecutorService().shutdownNow();
+      } else {
+        result = ImmutableList.of();
+      }
+      shutdown = true;
+      return result;
+    }
   }
 
   @Override
@@ -184,9 +205,12 @@ public class GaeScheduledExecutorService implements ScheduledExecutorService {
     ExecutorWrapper(String threadName) {
       GaeThreadFactory threadFactory = GaeThreadFactory.getInstance();
       if (threadFactory.isUsingBackgroundThreads()) {
+        // Create a thread pool with long-lived threads if background thread support is available.
         scheduledExecutorService = new RevivingScheduledExecutor(threadFactory, threadName, true);
         executorService = scheduledExecutorService;
       } else {
+        // Create an executor that creates a new thread for each submitted task, when background
+        // thread support is not available.
         scheduledExecutorService = null;
         executorService =
             new ThreadPoolExecutor(
diff --git a/src/main/java/com/google/firebase/internal/GaeThreadFactory.java b/src/main/java/com/google/firebase/internal/GaeThreadFactory.java
index 6150c37..f7e5b3f 100644
--- a/src/main/java/com/google/firebase/internal/GaeThreadFactory.java
+++ b/src/main/java/com/google/firebase/internal/GaeThreadFactory.java
@@ -36,7 +36,7 @@ public class GaeThreadFactory implements ThreadFactory {
   private static final Logger logger = LoggerFactory.getLogger(GaeThreadFactory.class);
 
   public static final ScheduledExecutorService DEFAULT_EXECUTOR =
-      new GaeScheduledExecutorService("FirebaseDefault");
+      new GaeScheduledExecutorService("LegacyFirebaseDefault");
   private static final String GAE_THREAD_MANAGER_CLASS = "com.google.appengine.api.ThreadManager";
   private static final GaeThreadFactory instance = new GaeThreadFactory();
   private final AtomicReference<ThreadFactoryWrapper> threadFactory = new AtomicReference<>(null);
diff --git a/src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java b/src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java
index a381367..7ea65c8 100644
--- a/src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java
+++ b/src/main/java/com/google/firebase/internal/RevivingScheduledExecutor.java
@@ -42,8 +42,9 @@ public class RevivingScheduledExecutor extends ScheduledThreadPoolExecutor {
   private static final Logger logger = LoggerFactory.getLogger(RevivingScheduledExecutor.class);
 
   /** Exception to throw to shut down the core threads. */
-  private static final RuntimeException REVIVE_THREAD_EXCEPTION =
-      new RuntimeException("Restarting Firebase Worker Thread");
+  private static final RuntimeException REVIVE_THREAD_EXCEPTION = new RuntimeException(
+      "Restarting Firebase Worker Thread. This exception is expected to occur periodically "
+          + "when deployed in the App Engine environment, and can be ignored.");
 
   /** 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);
diff --git a/src/main/java/com/google/firebase/tasks/Task.java b/src/main/java/com/google/firebase/tasks/Task.java
index 50705eb..4664ee8 100644
--- a/src/main/java/com/google/firebase/tasks/Task.java
+++ b/src/main/java/com/google/firebase/tasks/Task.java
@@ -73,6 +73,8 @@ public abstract class Task<T> {
    * added, they will be called in the order in which they were added.
    *
    * @return this Task
+   *
+   * @deprecated Use {@link #addOnSuccessListener(Executor, OnSuccessListener)}
    */
   @NonNull
   public abstract Task<T> addOnSuccessListener(@NonNull OnSuccessListener<? super T> listener);
@@ -99,6 +101,8 @@ public abstract class Task<T> {
    * called in the order in which they were added.
    *
    * @return this Task
+   *
+   * @deprecated Use {@link #addOnFailureListener(Executor, OnFailureListener)}
    */
   @NonNull
   public abstract Task<T> addOnFailureListener(@NonNull OnFailureListener listener);
@@ -124,6 +128,8 @@ public abstract class Task<T> {
    * called in the order in which they were added.
    *
    * @return this Task
+   *
+   * @deprecated Use {@link #addOnCompleteListener(Executor, OnCompleteListener)}
    */
   @NonNull
   public Task<T> addOnCompleteListener(@NonNull OnCompleteListener<T> listener) {
@@ -152,6 +158,8 @@ public abstract class Task<T> {
    * <p>If the Continuation throws an exception, the returned Task will fail with that exception.
    *
    * <p>The Continuation will be called on a shared thread pool.
+   *
+   * @deprecated Use {@link #continueWith(Executor, Continuation)}.
    */
   @NonNull
   public <R> Task<R> continueWith(@NonNull Continuation<T, R> continuation) {
@@ -180,6 +188,8 @@ public abstract class Task<T> {
    * <p>If the Continuation throws an exception, the returned Task will fail with that exception.
    *
    * <p>The Continuation will be called on a shared thread pool.
+   *
+   * @deprecated Use {@link #continueWithTask(Executor, Continuation)}
    */
   @NonNull
   public <R> Task<R> continueWithTask(@NonNull Continuation<T, Task<R>> continuation) {
diff --git a/src/main/java/com/google/firebase/tasks/TaskExecutors.java b/src/main/java/com/google/firebase/tasks/TaskExecutors.java
index 962fa21..19674d1 100644
--- a/src/main/java/com/google/firebase/tasks/TaskExecutors.java
+++ b/src/main/java/com/google/firebase/tasks/TaskExecutors.java
@@ -16,18 +16,28 @@
 
 package com.google.firebase.tasks;
 
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.firebase.internal.GaeThreadFactory;
 import com.google.firebase.internal.NonNull;
 
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
 
 /** 
  * Standard {@link Executor} instances for use with {@link Task}.
+ *
+ * @deprecated Use the ThreadManager interface to get required Executors.
  */
 public class TaskExecutors {
 
-  /** An Executor that uses a shared cached thread pool. */
+  /**
+   * An Executor that uses a shared cached thread pool.
+   *
+   * <p>This is no longer used in the SDK code. All the methods that submit to this thread pool
+   * have been deprecated, and their invocations have been routed elsewhere. This is left here
+   * for now for backward compatibility, since technically it is part of the public API.
+   */
   public static final Executor DEFAULT_THREAD_POOL;
   /** An Executor that uses the calling thread. */
   static final Executor DIRECT =
@@ -42,7 +52,11 @@ public class TaskExecutors {
     if (GaeThreadFactory.isAvailable()) {
       DEFAULT_THREAD_POOL = GaeThreadFactory.DEFAULT_EXECUTOR;
     } else {
-      DEFAULT_THREAD_POOL = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
+      ThreadFactory threadFactory = new ThreadFactoryBuilder()
+          .setNameFormat("task-exec-%d")
+          .setDaemon(true)
+          .build();
+      DEFAULT_THREAD_POOL = Executors.newCachedThreadPool(threadFactory);
     }
   }
 
diff --git a/src/main/java/com/google/firebase/tasks/Tasks.java b/src/main/java/com/google/firebase/tasks/Tasks.java
index 7ece8e2..3671ac1 100644
--- a/src/main/java/com/google/firebase/tasks/Tasks.java
+++ b/src/main/java/com/google/firebase/tasks/Tasks.java
@@ -32,6 +32,8 @@ import java.util.concurrent.TimeoutException;
 
 /** 
  * {@link Task} utility methods.
+ *
+ * @deprecated Use ApiFutures and ThreadManager interface instead.
  */
 public final class Tasks {
 
@@ -55,6 +57,8 @@ public final class Tasks {
    * Returns a Task that will be completed with the result of the specified Callable.
    *
    * <p>The Callable will be called on a shared thread pool.
+   *
+   * @deprecated Use {@link #call(Executor, Callable)}
    */
   public static <T> Task<T> call(@NonNull Callable<T> callable) {
     return call(TaskExecutors.DEFAULT_THREAD_POOL, callable);


Test patch:
diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java
index b60a598..07735f6 100644
--- a/src/test/java/com/google/firebase/FirebaseAppTest.java
+++ b/src/test/java/com/google/firebase/FirebaseAppTest.java
@@ -206,7 +206,7 @@ public class FirebaseAppTest {
         Arrays.asList(
             "delete",
             "equals",
-            "getListeners",
+            "getName",
             "getPersistenceKey",
             "hashCode",
             "isDefaultApp",
diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
index 1d9bab7..f043336 100644
--- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java
+++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java
@@ -36,6 +36,9 @@ import com.google.firebase.tasks.Task;
 import com.google.firebase.testing.ServiceAccount;
 import com.google.firebase.testing.TestUtils;
 import java.io.IOException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+
 import org.junit.Test;
 
 /** 
@@ -55,6 +58,22 @@ public class FirebaseOptionsTest {
           .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream()))
           .build();
 
+  private static final ThreadManager MOCK_THREAD_MANAGER = new ThreadManager() {
+    @Override
+    protected ScheduledExecutorService getExecutor(FirebaseApp app) {
+      return null;
+    }
+
+    @Override
+    protected void releaseExecutor(FirebaseApp app, ScheduledExecutorService executor) {
+    }
+
+    @Override
+    protected ThreadFactory getThreadFactory() {
+      return null;
+    }
+  };
+
   @Test
   public void createOptionsWithAllValuesSet() throws IOException, InterruptedException {
     GsonFactory jsonFactory = new GsonFactory();
@@ -67,12 +86,14 @@ public class FirebaseOptionsTest {
             .setProjectId(FIREBASE_PROJECT_ID)
             .setJsonFactory(jsonFactory)
             .setHttpTransport(httpTransport)
+            .setThreadManager(MOCK_THREAD_MANAGER)
             .build();
     assertEquals(FIREBASE_DB_URL, firebaseOptions.getDatabaseUrl());
     assertEquals(FIREBASE_STORAGE_BUCKET, firebaseOptions.getStorageBucket());
     assertEquals(FIREBASE_PROJECT_ID, firebaseOptions.getProjectId());
     assertSame(jsonFactory, firebaseOptions.getJsonFactory());
     assertSame(httpTransport, firebaseOptions.getHttpTransport());
+    assertSame(MOCK_THREAD_MANAGER, firebaseOptions.getThreadManager());
 
     GoogleCredentials credentials = firebaseOptions.getCredentials();
     assertNotNull(credentials);
@@ -90,6 +111,7 @@ public class FirebaseOptionsTest {
             .build();
     assertNotNull(firebaseOptions.getJsonFactory());
     assertNotNull(firebaseOptions.getHttpTransport());
+    assertNotNull(firebaseOptions.getThreadManager());
     assertNull(firebaseOptions.getDatabaseUrl());
     assertNull(firebaseOptions.getStorageBucket());
 
@@ -147,6 +169,19 @@ public class FirebaseOptionsTest {
     new FirebaseOptions.Builder().build();
   }
 
+  @Test(expected = NullPointerException.class)
+  public void createOptionsWithNullCredentials() {
+    new FirebaseOptions.Builder().setCredentials(null).build();
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void createOptionsWithNullThreadManager() {
+    new FirebaseOptions.Builder()
+        .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream()))
+        .setThreadManager(null)
+        .build();
+  }
+
   @Test
   public void checkToBuilderCreatesNewEquivalentInstance() {
     FirebaseOptions allValuesOptionsCopy = new FirebaseOptions.Builder(ALL_VALUES_OPTIONS).build();
@@ -156,6 +191,7 @@ public class FirebaseOptionsTest {
     assertEquals(ALL_VALUES_OPTIONS.getProjectId(), allValuesOptionsCopy.getProjectId());
     assertEquals(ALL_VALUES_OPTIONS.getJsonFactory(), allValuesOptionsCopy.getJsonFactory());
     assertEquals(ALL_VALUES_OPTIONS.getHttpTransport(), allValuesOptionsCopy.getHttpTransport());
+    assertEquals(ALL_VALUES_OPTIONS.getThreadManager(), allValuesOptionsCopy.getThreadManager());
   }
 
   @Test
diff --git a/src/test/java/com/google/firebase/TestOnlyImplFirebaseTrampolines.java b/src/test/java/com/google/firebase/TestOnlyImplFirebaseTrampolines.java
index 77c6be2..cd29dbb 100644
--- a/src/test/java/com/google/firebase/TestOnlyImplFirebaseTrampolines.java
+++ b/src/test/java/com/google/firebase/TestOnlyImplFirebaseTrampolines.java
@@ -19,6 +19,7 @@ package com.google.firebase;
 import com.google.auth.oauth2.AccessToken;
 import com.google.auth.oauth2.GoogleCredentials;
 import java.io.IOException;
+import java.util.concurrent.Callable;
 
 /**
  * Provides trampolines into package-private APIs used by components of Firebase
@@ -56,4 +57,13 @@ public final class TestOnlyImplFirebaseTrampolines {
   public static GoogleCredentials getCredentials(FirebaseOptions options) {
     return options.getCredentials();
   }
+
+  public static void forceThreadManagerInit(FirebaseApp app) {
+    ImplFirebaseTrampolines.submitCallable(app, new Callable<Object>() {
+      @Override
+      public Object call() throws Exception {
+        return null;
+      }
+    });
+  }
 }
diff --git a/src/test/java/com/google/firebase/ThreadManagerTest.java b/src/test/java/com/google/firebase/ThreadManagerTest.java
new file mode 100644
index 0000000..e89896e
--- /dev/null
+++ b/src/test/java/com/google/firebase/ThreadManagerTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.MockGoogleCredentials;
+import com.google.firebase.internal.NonNull;
+import com.google.firebase.internal.Nullable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import org.junit.After;
+import org.junit.Test;
+
+public class ThreadManagerTest {
+
+  @After
+  public void tearDown() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testBareBonesAppLifecycle() {
+    MockThreadManager threadManager = new MockThreadManager();
+
+    // Initializing an app should initialize the executor.
+    FirebaseApp app = FirebaseApp.initializeApp(buildOptions(threadManager));
+    assertEquals(1, threadManager.events.size());
+    Event event = threadManager.events.get(0);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+
+    // Deleting the app should clean up the executor.
+    app.delete();
+    assertEquals(2, threadManager.events.size());
+    event = threadManager.events.get(1);
+    assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+  }
+
+  @Test
+  public void testBareBonesAppLifecycleWithMultipleApps() {
+    MockThreadManager threadManager = new MockThreadManager();
+
+    // Initializing an app should initialize the executor.
+    final FirebaseApp app1 = FirebaseApp.initializeApp(buildOptions(threadManager));
+    assertEquals(1, threadManager.events.size());
+    Event event = threadManager.events.get(0);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app1, event.app);
+
+    final FirebaseApp app2 = FirebaseApp.initializeApp(buildOptions(threadManager), "otherApp");
+    assertEquals(2, threadManager.events.size());
+    event = threadManager.events.get(1);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app2, event.app);
+
+    // Deleting the app should clean up the executor.
+    app1.delete();
+    assertEquals(3, threadManager.events.size());
+    event = threadManager.events.get(2);
+    assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type);
+    assertEquals(app1, event.app);
+
+    app2.delete();
+    assertEquals(4, threadManager.events.size());
+    event = threadManager.events.get(3);
+    assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type);
+    assertEquals(app2, event.app);
+  }
+
+  @Test
+  public void testAppLifecycleWithExecutor() {
+    MockThreadManager threadManager = new MockThreadManager();
+
+    // Initializing an app should initialize the executor.
+    FirebaseApp app = FirebaseApp.initializeApp(buildOptions(threadManager));
+    assertEquals(1, threadManager.events.size());
+    Event event = threadManager.events.get(0);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+
+    // Should not re-initialize
+    TestOnlyImplFirebaseTrampolines.forceThreadManagerInit(app);
+    assertEquals(1, threadManager.events.size());
+
+    // Deleting app should tear down threading resources.
+    app.delete();
+    assertEquals(2, threadManager.events.size());
+    event = threadManager.events.get(1);
+    assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+  }
+
+  @Test
+  public void testAppLifecycleWithTokenRefresh() {
+    MockThreadManager threadManager = new MockThreadManager();
+
+    // Initializing an app should initialize the executor.
+    FirebaseApp app = FirebaseApp.initializeApp(buildOptions(threadManager));
+    assertEquals(1, threadManager.events.size());
+    Event event = threadManager.events.get(0);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+
+    // Refreshing the token this way kicks off the proactive token refresh task, which initializes
+    // the executor to schedule the next refresh task.
+    TestOnlyImplFirebaseTrampolines.getToken(app, true);
+    assertEquals(1, threadManager.events.size());
+
+    // Should not re-initialize
+    TestOnlyImplFirebaseTrampolines.forceThreadManagerInit(app);
+    assertEquals(1, threadManager.events.size());
+
+    // Deleting app should tear down threading resources.
+    app.delete();
+    assertEquals(2, threadManager.events.size());
+    event = threadManager.events.get(1);
+    assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+  }
+
+  @Test
+  public void testAppLifecycleWithServiceCall() {
+    MockThreadManager threadManager = new MockThreadManager();
+
+    // Initializing an app should initialize the executor.
+    FirebaseApp app = FirebaseApp.initializeApp(buildOptions(threadManager));
+    assertEquals(1, threadManager.events.size());
+    Event event = threadManager.events.get(0);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+
+    // Initializing auth should not initialize any more threading resources.
+    FirebaseAuth auth = FirebaseAuth.getInstance(app);
+    assertEquals(1, threadManager.events.size());
+
+    // Calling an async method on auth should not re-initialize threading resources.
+    try {
+      auth.verifyIdTokenAsync("foo").get();
+    } catch (InterruptedException | ExecutionException ignored) {
+      // ignored
+    }
+    assertEquals(1, threadManager.events.size());
+
+    // Deleting app should tear down threading resources.
+    app.delete();
+    assertEquals(2, threadManager.events.size());
+    event = threadManager.events.get(1);
+    assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+  }
+
+  @Test
+  public void testAppLifecycleWithMultipleServiceCalls()
+      throws ExecutionException, InterruptedException {
+    MockThreadManager threadManager = new MockThreadManager();
+
+    // Initializing an app should initialize the executor.
+    FirebaseApp app = FirebaseApp.initializeApp(buildOptions(threadManager));
+    assertEquals(1, threadManager.events.size());
+    Event event = threadManager.events.get(0);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+
+    // Initializing auth should not re-initialize threading resources.
+    FirebaseAuth auth = FirebaseAuth.getInstance(app);
+    assertEquals(1, threadManager.events.size());
+
+    // Calling an async method on auth should not re-initialize threading resources.
+    try {
+      auth.verifyIdTokenAsync("foo").get();
+    } catch (InterruptedException | ExecutionException ignored) {
+      // ignored
+    }
+    assertEquals(1, threadManager.events.size());
+    event = threadManager.events.get(0);
+    assertEquals(Event.TYPE_GET_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+
+    // Calling a method again should not re-initialize threading resources.
+    try {
+      auth.verifyIdTokenAsync("foo").get();
+    } catch (InterruptedException | ExecutionException ignored) {
+      // ignored
+    }
+    assertEquals(1, threadManager.events.size());
+
+    // Should not re-initialize threading resources.
+    TestOnlyImplFirebaseTrampolines.forceThreadManagerInit(app);
+    assertEquals(1, threadManager.events.size());
+
+    // Deleting app should tear down threading resources.
+    app.delete();
+    assertEquals(2, threadManager.events.size());
+    event = threadManager.events.get(1);
+    assertEquals(Event.TYPE_RELEASE_EXECUTOR, event.type);
+    assertEquals(app, event.app);
+  }
+
+  private FirebaseOptions buildOptions(ThreadManager threadManager) {
+    return new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials())
+        .setProjectId("mock-project-id")
+        .setThreadManager(threadManager)
+        .build();
+  }
+
+  public static class MockThreadManager extends ThreadManager {
+
+    private final List<Event> events = new ArrayList<>();
+
+    @Override
+    protected ScheduledExecutorService getExecutor(@NonNull FirebaseApp app) {
+      events.add(new Event(Event.TYPE_GET_EXECUTOR, app, null));
+      return Executors.newSingleThreadScheduledExecutor();
+    }
+
+    @Override
+    protected void releaseExecutor(@NonNull FirebaseApp app,
+        @NonNull ScheduledExecutorService executor) {
+      events.add(new Event(Event.TYPE_RELEASE_EXECUTOR, app, executor));
+      executor.shutdownNow();
+    }
+
+    @Override
+    protected ThreadFactory getThreadFactory() {
+      events.add(new Event(Event.TYPE_GET_THREAD_FACTORY, null, null));
+      return Executors.defaultThreadFactory();
+    }
+  }
+  
+  private static class Event {
+    private static final int TYPE_GET_EXECUTOR = 100;
+    private static final int TYPE_RELEASE_EXECUTOR = 101;
+    private static final int TYPE_GET_THREAD_FACTORY = 102;
+
+    private final int type;
+    private final FirebaseApp app;
+    private ScheduledExecutorService executor;
+
+    public Event(int type, @Nullable FirebaseApp app, @Nullable ScheduledExecutorService executor) {
+      this.type = type;
+      this.app = app;
+      this.executor = executor;
+    }
+  }
+}
diff --git a/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java b/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java
index 791da4f..37ad6de 100644
--- a/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java
+++ b/src/test/java/com/google/firebase/database/core/JvmPlatformTest.java
@@ -21,14 +21,62 @@ import static org.junit.Assert.assertEquals;
 import com.google.firebase.FirebaseApp;
 import com.google.firebase.FirebaseOptions;
 import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.ThreadManager;
 import com.google.firebase.database.FirebaseDatabase;
+import com.google.firebase.internal.NonNull;
 import com.google.firebase.testing.ServiceAccount;
 import com.google.firebase.testing.TestUtils;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.junit.Assert;
 import org.junit.Test;
 
 public class JvmPlatformTest {
 
+  @Test
+  public void usesThreadManager() {
+    final AtomicInteger count = new AtomicInteger(0);
+    ThreadManager threadManager = new ThreadManager() {
+      @Override
+      protected ScheduledExecutorService getExecutor(@NonNull FirebaseApp app) {
+        return Executors.newSingleThreadScheduledExecutor();
+      }
+
+      @Override
+      protected void releaseExecutor(@NonNull FirebaseApp app,
+          @NonNull ScheduledExecutorService executor) {
+      }
+
+      @Override
+      protected ThreadFactory getThreadFactory() {
+        count.incrementAndGet();
+        return Executors.defaultThreadFactory();
+      }
+    };
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(TestUtils.getCertCredential(ServiceAccount.EDITOR.asStream()))
+        .setThreadManager(threadManager)
+        .build();
+    FirebaseApp app = FirebaseApp.initializeApp(options, "threadManagerApp");
+
+    try {
+      assertEquals(0, count.get());
+      Context cfg = new DatabaseConfig();
+      cfg.firebaseApp = app;
+      cfg.freeze();
+      // EventTarget and RunLoop
+      assertEquals(2, count.get());
+
+      cfg.getConnectionContext();
+      // ConnectionContext which gets passed to all low-level socket management code.
+      assertEquals(3, count.get());
+    } finally {
+      TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+    }
+  }
+
   @Test
   public void userAgentHasCorrectParts() {
     FirebaseOptions options = new FirebaseOptions.Builder()
diff --git a/src/test/java/com/google/firebase/internal/FirebaseExecutorsTest.java b/src/test/java/com/google/firebase/internal/FirebaseExecutorsTest.java
new file mode 100644
index 0000000..d8e7792
--- /dev/null
+++ b/src/test/java/com/google/firebase/internal/FirebaseExecutorsTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.MockGoogleCredentials;
+import com.google.firebase.internal.FirebaseExecutors.GlobalThreadManager;
+import com.google.firebase.tasks.Tasks;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import org.junit.After;
+import org.junit.Test;
+
+public class FirebaseExecutorsTest {
+
+  @After
+  public void tearDown() {
+    TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+  }
+
+  @Test
+  public void testGlobalThreadManager() {
+    MockThreadManager threadManager = new MockThreadManager();
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials())
+        .setThreadManager(threadManager)
+        .build();
+    FirebaseApp defaultApp = FirebaseApp.initializeApp(options);
+    assertEquals(1, threadManager.initCount);
+
+    ScheduledExecutorService exec1 = threadManager.getExecutor(defaultApp);
+    assertEquals(1, threadManager.initCount);
+    assertFalse(exec1.isShutdown());
+
+    ScheduledExecutorService exec2 = threadManager.getExecutor(defaultApp);
+    assertEquals(1, threadManager.initCount);
+    assertFalse(exec2.isShutdown());
+
+    // Should return the same executor for both invocations.
+    assertSame(exec1, exec2);
+
+    threadManager.releaseExecutor(defaultApp, exec1);
+    assertTrue(exec1.isShutdown());
+  }
+
+  @Test
+  public void testGlobalThreadManagerWithMultipleApps() {
+    MockThreadManager threadManager = new MockThreadManager();
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials())
+        .build();
+    FirebaseApp defaultApp = FirebaseApp.initializeApp(options);
+    FirebaseApp customApp = FirebaseApp.initializeApp(options, "customApp");
+    assertEquals(0, threadManager.initCount);
+
+    ScheduledExecutorService exec1 = threadManager.getExecutor(defaultApp);
+    ScheduledExecutorService exec2 = threadManager.getExecutor(customApp);
+    assertEquals(1, threadManager.initCount);
+    assertFalse(exec1.isShutdown());
+
+    // Should return the same executor for both invocations.
+    assertSame(exec1, exec2);
+
+    threadManager.releaseExecutor(defaultApp, exec1);
+    assertFalse(exec1.isShutdown());
+
+    threadManager.releaseExecutor(customApp, exec2);
+    assertTrue(exec1.isShutdown());
+  }
+
+  @Test
+  public void testGlobalThreadManagerReInit() {
+    MockThreadManager threadManager = new MockThreadManager();
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials())
+        .setThreadManager(threadManager)
+        .build();
+    FirebaseApp defaultApp = FirebaseApp.initializeApp(options);
+    assertEquals(1, threadManager.initCount);
+
+    ScheduledExecutorService exec1 = threadManager.getExecutor(defaultApp);
+    assertEquals(1, threadManager.initCount);
+    assertFalse(exec1.isShutdown());
+
+    // Simulate app.delete()
+    threadManager.releaseExecutor(defaultApp, exec1);
+    assertTrue(exec1.isShutdown());
+
+    // Simulate app re-init
+    ScheduledExecutorService exec2 = threadManager.getExecutor(defaultApp);
+    assertEquals(2, threadManager.initCount);
+    assertNotSame(exec1, exec2);
+
+    threadManager.releaseExecutor(defaultApp, exec2);
+    assertTrue(exec2.isShutdown());
+  }
+
+  @Test
+  public void testDefaultThreadManager() throws Exception {
+    FirebaseOptions options = new FirebaseOptions.Builder()
+        .setCredentials(new MockGoogleCredentials())
+        .build();
+    FirebaseApp defaultApp = FirebaseApp.initializeApp(options);
+    final Map<String, Object> threadInfo = new HashMap<>();
+    Callable<Void> command = new Callable<Void>() {
+      @Override
+      public Void call() throws Exception {
+        Thread thread = Thread.currentThread();
+        threadInfo.put("name", thread.getName());
+        threadInfo.put("daemon", thread.isDaemon());
+        return null;
+      }
+    };
+    Tasks.await(ImplFirebaseTrampolines.submitCallable(defaultApp, command));
+
+    // Check for default JVM thread properties.
+    assertTrue(threadInfo.get("name").toString().startsWith("firebase-default-"));
+    assertTrue((Boolean) threadInfo.get("daemon"));
+
+    defaultApp.delete();
+    try {
+      ImplFirebaseTrampolines.submitCallable(defaultApp, command);
+      fail("No error thrown when submitting to deleted app");
+    } catch (RejectedExecutionException expected) {
+      // expected
+    }
+  }
+
+  private static class MockThreadManager extends GlobalThreadManager {
+
+    private int initCount = 0;
+
+    @Override
+    protected ScheduledExecutorService doInit() {
+      initCount++;
+      return Executors.newSingleThreadScheduledExecutor();
+    }
+
+    @Override
+    protected void doCleanup(ScheduledExecutorService executorService) {
+      executorService.shutdownNow();
+    }
+
+    @Override
+    protected ThreadFactory getThreadFactory() {
+      return Executors.defaultThreadFactory();
+    }
+  }
+}