Firebase: Asynchronous Operations with Admin Java SDK

Hiranya Jayathilaka
Google Cloud - Community
7 min readDec 5, 2017

--

Version 5.4.0 of the Firebase Admin Java SDK made significant improvements to how threads and asynchronous operations are handled by the SDK. Among the changes, the deprecation of the Task interface, and introduction of the ApiFuture interface are perhaps the most noteworthy. These changes cut across multiple APIs exposed by the Admin Java SDK, and have a noticeable impact on how developers write code using the SDK. This post discusses the rationale behind these changes, and explains how to migrate a Java application from Tasks to ApiFutures.

Most public API methods in the Admin Java SDK are asynchronous. That is, the caller does not get blocked on the methods. The SDK simply submits the execution of the method body to a pool of worker threads, and immediately returns to the caller with an object that represents the submitted operation. Up until version 5.4.0 of the SDK, this returned object was an instance of Task<T> where the generic type parameter T represented the type of the result produced by the asynchronous operation. For example, the FirebaseAuth.createCustomToken() method for creating custom JWTs would return a Task<String>.

The Task interface supports registering callbacks that will fire when the underlying asynchronous operation completes. It also facilitates continuations — i.e. chaining multiple asynchronous operations, so that the result produced by one operation can be piped to another.

Version 5.4.0 of the Admin Java SDK deprecated the Task interface, as well as all the public API methods that return a Task<T>. The SDK now recommends using the newly introduced *Async() methods that return ApiFuture<T>. For instance, instead of FirebaseAuth.createCustomToken(), developers are now advised to use the FirebaseAuth.createCustomTokenAsync() method, which returns an ApiFuture<String>. Similar replacements have been introduced to all other public methods in the SDK. These new methods are functionally equivalent to their deprecated counterparts. That is, they also submit the method execution to a pool of worker threads, and return immediately. But the returned objects that represent asynchronous operations are of a different type.

Architecturally, Task and ApiFuture interfaces represent two distinct styles of asynchronous programming. Therefore they are seemingly quite different from each other. However, they can be used to implement the same use cases, and thus migrating from one to the other is often trivial, as we shall see shortly.

Why deprecate the Task interface?

This move was motivated by two main reasons:

  1. Most server-side Java libraries — including the libraries shipped by Google Cloud Platform (GCP)— expose asynchronous operations using Java’s built-in Future interface (or a child-interface of it). We wanted the Admin Java SDK to look more like those libraries, so that server-side Java developers would feel at home. We also wanted to provide a consistent developer experience when mixing Firebase and GCP libraries in the same application.
  2. The Task interface and the associated utilities were forked from the Android Google Mobile Services (GMS) API, during the early days of the Admin Java SDK. Keeping this code in sync with Android GMS, and maintaining it was becoming cumbersome.

The Task interface being an Android API should not surprise you. The callback-based programming style promoted by this API is prevalent in client-side platforms like Android. Client apps are usually tied to a graphical user interface (GUI). Hence they are expected to be highly interactive and responsive. Imagine a mobile app with a button, where some computation should be performed at the click of the button, and the results displayed on screen. Task is the perfect abstraction to implement this use case. The app can start a Task when the button is clicked, and the GUI update logic can be implemented as a callback to that Task. This way, the GUI worker thread that receives input from the user never gets blocked, and the GUI remains responsive the whole time.

In contrast, most server-side applications do not have to be interactive. They often do not have a GUI, and are instead designed for machine-to-machine interactions at scale. Consequently, server-side Java applications tend to prioritize scale and reduced end-to-end request latency over GUI experience. In this context, what developers typically need is the simple fork-join style of asynchronous programming. This is where a program would fork a set of potentially expensive operations in parallel threads, and later join them if necessary. The Future interface is great for that.

When taking these intricacies into consideration, it becomes apparent why an API based on Futures is better-suited for the Admin Java SDK. However, we did not want to completely give up on callbacks either. In many event-driven and reactive systems, callbacks are a must have (even in server-side). Therefore instead of the built-in Future interface, we chose one of its richer child-interfaces— ApiFuture from the Google API Common project. ApiFutures offer best of both worlds. They are derived from the same built-in Future interface of Java, but also support adding callbacks. Moreover, several GCP libraries have also adopted ApiFutures, which is all the more reason to use the same interface in Firebase Admin Java SDK.

Migrating from Tasks to ApiFutures

What follows is a set of code samples that demonstrate how to migrate Java code from Tasks to ApiFutures. Each sample shows a certain feature of Tasks, and the equivalent code using ApiFutures. If you have any code that uses Tasks, chances are you are making generous use of callbacks. Therefore, lets begin by demonstrating how to add callbacks to an ApiFuture.

Listing 1: Adding callbacks to a Task and an ApiFuture

There are few noteworthy points about this example:

  1. The Task interface directly exposes the methods for adding callbacks (e.g. addOnSuccessListener() and addOnFailureListener()). But to add a callback to an ApiFuture, one must use the static addCallback() method in the ApiFutures helper class.
  2. The Task interface facilitates adding separate callbacks to handle success, failure and completion events. The ApiFuture on the other hand has a less flexible API for adding callbacks, in the sense it only supports one type of callbacks — ApiFutureCallback which handles both success and failure events.
  3. In both APIs interfaces there is no limit to the number of callbacks that can be added, and there are no guarantees concerning the order of callbacks when they execute.

An inquisitive developer might like to know on which thread the ApiFutureCallback added in listing 1 gets executed. If the underlying asynchronous operation has not yet completed by the time addCallback() is invoked, the callback will run on the same thread as the asynchronous operation, whenever it completes. If the operation has already completed, the callback runs immediately on the thread that called addCallback(). This behavior is good enough when the callbacks are simple and short-lived. If your callbacks are expensive or if you wish to have more predictable semantics with respect to this, you should use the ApiFutures.addCallback() override that accepts an Executor as a third argument. [Caveat: This override is only available since version 1.2.0 of Google API Common, whereas the latest Admin SDK (5.5.0) ships with 1.1.0. Until this gets resolved, one must upgrade Google API Common manually to use the new method.]

Next we shall discuss how to migrate Task continuations. A continuation transforms the output of an asynchronous operation, and exposes the aggregate computation (asynchronous operation+transformation) as a new Task. The recursive nature of this action effectively enables chaining sequences of asynchronous operations. Listing 2 shows a continuation that transforms the string produced by a Task<String> into a list of strings. The resulting aggregate computation is represented by an instance of Task<List<String>>.

Listing 2: Continuations with Tasks and ApiFutures

In this example, the ApiFunction interface plays the role of the now deprecated Continuation interface. The ApiFutures.transform() helper method exposes the resulting aggregate computation as an instance of ApiFuture<List<String>>. There is also an ApiAsyncFunction interface, and an ApiFutures.transformAsync() helper method for cases where the transformation itself needs to make asynchronous calls.

Listing 3 demonstrates how to wrap values and exceptions in ApiFutures. There’s little cause for a developer using Admin SDK to use it, since the SDK already provides methods that return ApiFutures. It is mentioned here for completeness.

Listing 3: Wrapping values and exceptions

Be mindful when adding callbacks or transformations to an immediate ApiFuture. They will always execute on the calling thread of the addCallback() or transform() method. If this is not the desired behavior, an explicit Executor should be specified when calling those helper methods.

Listing 4 illustrates how to wait until an asynchronous operation completes. The Tasks helper class is required to wait on a Task. But it is rather trivial with an ApiFuture.

Listing 4: Waiting for Tasks and ApiFutures

Sometimes we want to create an incomplete asynchronous operation, and complete it later. This is often useful when you want to represent an arbitrary chunk of code as an asynchronous operation. With Tasks this was achieved using the TaskCompletionSource. Our last code sample in listing 5 shows how to replace a TaskCompletionSource with an ApiFuture.

Listing 5: Incomplete asynchronous operations

Conclusion

Since the 5.4.0 release of the Java Admin SDK, I’ve seen several inquiries for more details and examples regarding the ApiFuture interface. I hope this post addresses them, while alleviating some of the tensions around Task to ApiFuture migration. I also invite the Firebase Java community to take a look at the newly added ThreadManager interface, which enables configuring thread pools and thread factories for the Admin SDK. It provides far greater control over how the SDK schedules and executes asynchronous operations.

Going forward, we can expect more features, stability and documentation around the ApiFuture support available in the Admin SDK. Firebase is also making it easier to seamlessly mix and match Admin SDK APIs with various GCP libraries (there’s already support for Google Cloud Storage and Google Cloud Firestore). If you’re following the Java Admin SDK GitHub repo, you may have seen that there’s also an effort towards exposing a set of blocking APIs from the SDK. Share your thoughts in comments as well as on GitHub. Let us know how you use the Java Admin SDK, and how the developer experience can be further improved.

--

--

Hiranya Jayathilaka
Google Cloud - Community

Software engineer at Shortwave. Ex-Googler. PhD in CS. Enjoys working on cloud, mobile and programming languages. Fan of all things tech and open source.