Search This Blog

Advanced ListenableFuture capabilities

Last time we familiarized ourselves with ListenableFuture. I promised to introduced more advanced techniques, namely transformations and chaining. Let's start from something straightforward. Say we have our ListenableFuture<String> which we got from some asynchronous service. We also have a simple method:

Document parse(String xml) {//...

We don't need String, we need Document. One way would be to simply resolve Future (wait for it) and do the processing on String. But much more elegant solution is to apply transformation once the results are available and treat our method as if was always returning ListenableFuture<Document>. This is pretty straightforward:

Java syntax is a bit limiting, but please focus on what we just did. Futures.transform() doesn't wait for underlying ListenableFuture<String> to apply parse() transformation. Instead, under the hood, it registers a callback, wishing to be notified whenever given future finishes. This transformation is applied dynamically and transparently for us at right moment. We still have Future, but this time wrapping Document.

So let's go one step further. We also have an asynchronous, possibly long-running method that calculates relevance (whatever that is in this context) of a given Document:

Please look very carefully at all types and results. Notice the difference between Function and AsyncFunction. Initially we got an asynchronous method returning future of String. Later on we transformed it to seamlessly turn String into XML Document. This transformation happens asynchronously, when inner future completes. Having future of Document we would like to call a method that requires Document and returns future of Double.

If we call relevanceFuture.get(), our Future object will first wait for inner task to complete and having its result (String -> Document) will wait for outer task and return Double. We can also register callbacks on relevanceFuture which will fire when outer task (calculateRelevance()) finishes. If you are still here, the are even more crazy transformations.

Remember that all this happens in a loop. For each web site we got ListenableFuture<String> which we asynchronously transformed to ListenableFuture<Double>. So in the end we work with a List<ListenableFuture<Double>>. This also means that in order to extract all the results we either have to register listener for each and every ListenableFuture or wait for each of them. Which doesn't progress us at all. But what if we could easily transform from List<ListenableFuture<Double>> to ListenableFuture<List<Double>>? Read carefully - from list of futures to future of list. In other words, rather than having a bunch of small futures we have one future that will complete when all child futures complete - and the results are mapped one-to-one to target list. Guess what, Guava can do this!

Of course there is no waiting here as well. Wrapper ListenableFuture<List<Double>> will be notified every time one of its child futures complete. The moment the last child ListenableFuture<Double> completes, outer future completes as well. Everything is event-driven and completely hidden from you.

Do you think that's it? Say we would like to compute the biggest relevance in the whole set. As you probably know by now, we won't wait for a List<Double>. Instead we will register transformation from List<Double> to Double!

Was it worth it? Yes and no. Yes, because we learned some really important constructs and primitives used together with futures/promises: chaining, mapping (transforming) and reducing. The solution is beautiful in terms of CPU utilization - no waiting, blocking, etc. Remember that the biggest strength of Node.js is its "no-blocking" policy. Also in Netty futures are ubiquitous. Last but not least, it feels very functional.

On the other hand, mainly due to Java syntax verbosity and lack of type inference (yes, we will jump into Scala soon) code seems very unreadable, hard to follow and maintain. Well, to some degree this holds true for all message driven systems. But as long as we don't invent better APIs and primitives, we must learn to live and take advantage of asynchronous, highly parallel computations.

Your code was garbled by Blogspot, but looks like it works as it should. onSuccess callback is executed asynchronously, most likely after your return statements. That's why the allStringArray is empty. Instead you should return ListenableFuture or list of ListenableFutures.

Hi Tom, I was wondering if you could tell me something. I know that spring has its own implementation of ListenableFuture, but does it have a similar implementation of Futures.transform? I'm trying to find a way to chain operations together using the classes spring provides, but I only see examples using Guava.