systems: RPC (remote procedure call),
timeout, reading a file from disk, receiving the next event from an event stream.
With the help of a set of higher-order functions (called combinators),
futures can be combined freely to express more complex operations. These
combinations usually fall into one of
two composition categories: sequential or concurrent.
Sequential composition permits defining a future as a function of another,
such that the two are executed sequentially. This is useful where data dependency exists between two operations:
the result of the first future is needed
to compute the second future. For example, when a user sends a Tweet, we
first need to see if that user is within
the hourly rate limits before writing the
Tweet to a database. In the Figure 1 example, the future done represents this
composite operation. (For historical
reasons, the sequencing combinator is
called flatMap.)
This also shows how failures are
expressed in futures: Future.excep-tion returns a future that has already
completed in a failure state. In the case
of rate limiting, done becomes a failed
future (with the exception RateLimitingError). Failure short-circuits
any further composition: if, in the previous example, the future returned by
isRateLimited(user) fails, then
done is immediately failed; the closure
passed to flatMap is not run.
Another set of combinators defines
concurrent composition, allowing multiple futures to be combined when no
data dependencies exist among them.
Concurrent combinators turn a list
of futures into a future of a list of values. For example, you may have a list
of futures representing RPCs to all the
shards of a search index. The concurrent combinator collect turns this list of
futures into a future of the list of results.
val results: List[Future[String]] = …
val all: Future[List[String]] =
Future.collect(results)
Independent futures are executed
concurrently by default; execution is
sequenced only where data dependencies exist.
Future combinators never modify
the underlying future; instead, they
return a new future that represents
type of software. First, scale implies
concurrency. For example, a search
engine may split its index into many
small pieces (shards) so the entire cor-
pus can fit in main memory. To satisfy
queries efficiently, all shards must be
queried concurrently. Second, com-
munication between servers is asyn-
chronous and must be handled con-
currently for efficiency and safety.
Concurrent programming is traditionally approached by employing
threads and locks3—threads furnish
the programmer with concurrent
threads of execution, while locks coordinate the sharing of (mutable) data
across multiple threads.
In practice, threads and locks are
notoriously difficult to get right.
9 They
are hard to reason about, and they are
a stubborn cause of nasty bugs. What’s
more, they are difficult to compose: you
cannot safely and arbitrarily combine
a set of threads and locks to construct
new functionality. Their semantics of
computation are wrapped up in the mechanics of managing concurrency.
At Twitter, we instead structure concurrent programs around futures. A
future is an effect that represents the
result of an asynchronous operation.
It’s a type of reference cell that can be
in one of three states: incomplete, when
the future has not yet taken on a value;
completed with a value, when the future
holds the result of a successful operation; and completed with a failure, when
the operation has failed. Futures can
undergo at most one state transition:
from the incomplete state to either the
success or failure state.
In the following example, using Scala,
the future count represents the result
of an integer-valued operation. We
respond to the future’s completion directly: the block of code after respond
is a callback that is invoked when the
future has completed. (As you will see
shortly, we rarely respond directly to future completions in this way.)
val count: Future[Int] = getCount()
count.respond {
case Return(value) =>
println(s”The count was $value”)
case Throw(exc) =>
println(s”getCount failed with $exc”)
}
Futures represent just about every
asynchronous operation in Twitter
Futures, services,
and filters are
combined to build
server software
in a piecemeal
fashion. They let
programmers
build up complex
software while
preserving
their ability
to reason about
the correctness
of its constituent
parts.