the composite operation. This is an important tool for reasoning: composite
operations do not change the behavior
of their constituent parts. Indeed, a
single future can be used and reused in
many different compositions.
Let’s modify the earlier getCount
example to see how composition
allows building complex behavior
piecemeal. In distributed systems, it
is often preferable to degrade gracefully (for example, where a default
value or guess may exist) than to fail
an entire operation.
8 This can be
implemented by using a timeout for
the getCount operation, and, upon
failure, returning a default value of
0. This behavior can be expressed as
a compound operation among different futures. Specifically, you want
the future that represents the winner of a timeout-with-default and the
getCount operation (see Figure 2).
The future finalCount now represents the composite operation as described. This example has a number of
notable features. First, we have created
a composite operation using simple,
underlying parts—futures and functions. Second, the constituent futures
preserve their semantics under composition—their behavior does not
change, and they may be used in multiple different compositions. Third,
nothing has been said about the mechanics of execution; instead, the
composite computation is expressed
as the combination of a number of
underlying parts. No threads were
explicitly created, nor was there any explicit communication between them—
it is all implied by data dependencies.
This neatly illustrates how futures
liberate the application’s semantics
(what is computed) from its mechanics
(how it is computed). The programmer
composes concurrent operations but
need not specify how they are scheduled or how values are communicated.
This is a good separation of concerns:
application logic is not entangled with
the minutiae of runtime concerns.
Futures can sometimes free pro-
grammers from having to use locks.
Where data dependencies are wit-
nessed by composition of futures, the
implementation is responsible for
concurrency control. Put another way,
futures can be composed into a de-
pendency graph that is executed in the
manner of dataflow programming.
11
While we need to resort to explicit con-
currency control from time to time, a
large set of common use cases are han-
dled directly by the use of futures.
At Twitter, we implement the machinery required to make concurrent
execution with futures work in our
open-source Finagle4, 5 and Util6
libraries. These take care of mapping
execution onto OS threads through
a pluggable scheduler mechanism.
Some teams at Twitter have used
this capacity to construct scheduling strategies that better match their
problem domain and its attendant
trade offs. We have also used this capability to add features such as maintaining runtime statistics and Dap-per-style RPC tracing to our systems,
without changing any existing APIs or
modifying existing user code.
Programming with
Services and Filters
“We should have some ways of coupling
programs like garden hose—screw in an-
other segment when it becomes necessary
to massage data in another way. This is the
way of I/O also.”
—Doug McIlroy
Thus, the modern software engineering practice is centered on how to
contain and manage complexity—to
package it up in ways that allow us to
reason about our application’s behavior. In this endeavor the goal is to balance simplicity and clarity with reusability and modularity.
What’s more, server software must
account for the realities of distributed
systems. For example, a search engine
might simply omit results from a failing
index shard so that it can return a partial
result rather than failing the query in
Figure 1. Admit request only when the user is within their rate limits.
def isRateLimited(user: Long): Future[Boolean] = ...
def writeTweet(user: Long, tweet: String): Future[Unit] = ...
val user = 12L
val tweet: String = “just setting up my twitter”
val done: Future[Unit] =
isRateLimited(user).flatMap {
case true => Future.exception(new RateLimitingError)
case false => writeTweet(user, tweet)
}
Figure 2. Degrade with a default value after timeout.
val count: Future[Int] = getCount()
// Future.sleep(x) returns a future which completes
// after the given amount of time.
val timeout: Future[Unit] = Future.sleep( 5.seconds)
// The map method on Future constructs a new Future
// which, after successful completion, applies the given
// function to the result. It returns a new Future
// representing this composite operation. In this case,
// we simply return a default value of 0 after the timeout.
val default: Future[Int] = timeout.map(unit => 0)
// Select composes two Futures, returning a new
// Future which represents the first future to complete.
val finalCount: Future[Int] = Future.select(count, default)