Firebase: Asynchronous Operations with Admin Java SDK
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:
- 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. - 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
.
There are few noteworthy points about this example:
- The Task interface directly exposes the methods for adding callbacks (e.g.
addOnSuccessListener()
andaddOnFailureListener()
). But to add a callback to anApiFuture
, one must use the staticaddCallback()
method in theApiFutures
helper class. - The
Task
interface facilitates adding separate callbacks to handle success, failure and completion events. TheApiFuture
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. - 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>>
.
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.
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
.
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
.
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.