Interning on Xandr’s Data Delivery team as a full stack developer this summer has exposed me to a myriad of new technical skills and tools. For the frontend component of my project I learned to build UI’s using React, and for the backend I learned how to manage applications using Kubernetes and expose my validation services via a custom API. The backend portion of my project largely involved using a toolkit called Vert.x to control the overall flow of the application and to enable powerful functionalities to each individual class within the application. Vert.x is an open source project that is designed for asynchronous programming to increase efficiency and enable running multiple tasks simultaneously. I am going to assume that you already know how verticles function in Vert.x, but go here to learn more about the main concepts of Vert.x.
In this blog, I will discuss:
1. Futures and their role in asynchronous programming
2. How to deal with nested futures and Vert.x’s CompositeFuture class
A common setup with verticles involves one verticle running as an API service (more about Vert.x web servers and clients) that asks another verticle to run database queries and return the results back to the API service and back to the client who sent the API request. But how does all of this occur simultaneously with the rest of the application? How do we ensure we don’t wait for a large database query to run? The amount of data we have access to is perpetually increasing, so queries can become quite large, especially at a big tech company like Xandr.
The answer is futures: futures enable asynchronous programming and allow for lengthy functions and services to run while concurrently running other processes. Futures are like mailboxes: when you wake up in the morning, you’re not sure if there is mail in the box yet, you just know that the box is at the end of the driveway and that it will receive mail at some arbitrary point today (when the delivery person comes). Likewise, a future is a wrapper that may or may not contain a value at a given point in time, but eventually it will contain a value — either a result or an error.
The main idea is that you have a pointer to the box that will eventually contain the result so that your program knows where to look when the task is completed. At that point, the future’s predefined handler will be run, which is set using the onComplete function:
Note: ar.result() is only populated if the future succeeds, and ar.cause() is only populated if the future fails.
Adding another future call outside of a given future will run both futures separately and simultaneously; however, Vert.x also supports sequential future calls. For example, if you need to wait for one future to complete before deciding whether or not to run a second future, you can use the .compose() method. In this example, we want to validate that a table exists, and if it does, then validate the columns; otherwise, do not try to validate the columns because the table does not exist and return an error.
Note: if the first future fails, the whole future sequence fails, and the handler is skipped. Compose is very useful for daisy chaining two or three dependent futures together, but what if there are more futures that need to be handled as a group?
Vert.x has a subclass of Future called CompositeFuture, which wraps a collection of Futures and handles the results of all of the Futures together upon completion. Both CompositeFuture.all() and CompositeFuture.join() return a single future that succeeds when all futures succeed and fails when one future fails, but CompositeFuture.all() fails immediately when one wrapped future fails, while CompositeFuture.join() will continue to run the rest of the futures even if one fails.
CompositeFutures are intuitive and useful in handling a group of futures, although it’s not always easy to get each wrapped future’s individual results if they do not all succeed. CompositeFuture.list() returns a list of results only if each future succeeds, otherwise it returns null. This makes it difficult if you want to display all of the errors that occur in each future to the user. This example shows that it is fairly simple if you have access to the original list, but if not, you would have to iterate through the futures using an index with resultAt(index) and causeAt(index), and then check which one is null, which can get messy.
Vert.x futures are powerful components of asynchronous programming because they allow for predefined handlers to process the results upon completion and can be grouped together sequentially or concurrently with .compose() or CompositeFuture respectively to handle multiple related futures. Using futures with the Vert.x framework specifically gives our teams at Xandr access to a myriad of tools with detailed documentation to maximize functionality and increase the power of our applications. I barely scratched the surface of the different tools that Vert.x offers, so if you want to learn more about it, I highly recommend learning about the main concepts of Vert.x and reading through the examples on the Vert.x website. Thank you for taking the time read my post, I hope you consider integrating Vert.x in your current and future projects!