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 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.
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
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
Futureinterface (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.
Taskinterface 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.
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
There are few noteworthy points about this example:
- The Task interface directly exposes the methods for adding callbacks (e.g.
addOnFailureListener()). But to add a callback to an
ApiFuture, one must use the static
addCallback()method in the
Taskinterface facilitates adding separate callbacks to handle success, failure and completion events. The
ApiFutureon the other hand has a less flexible API for adding callbacks, in the sense it only supports one type of callbacks —
ApiFutureCallbackwhich 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
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
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
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
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
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.