Sunday, September 28, 2014

Finagle, promises, local and dtab

Finally figured out how request context is defined in finagle. It was a bunch of things that had been puzzling me for some time and all started making sense together.

So what is a request context. To understand request context we first have to understand how computations work in finagle. The first two rules of finagle are that you do not block. So how then do you make network calls and other typically blocking operations? The answer is that you still, of course, perform all those computations but by not blocking. Each blocking computation returns a future and then you define future computations on that. This way you never block a thread, you just do your thing and leave the thread, defining the next steps when you get a chance again. In this way you can define what you want to do as a graph of these chained operations. It may sound complex but in practice it is very easy to express as monadic operations on futures. Scala even provides a convenient (for comprehension) syntax for expressing these. So you can imagine an incoming request coming in, starting a bunch of async computations that in turn spawn other computations and so on. Ultimately, a request ends up occupying chunks of execution time slices on many threads. Request Context is state that is available in all those time slices. If all these operations were happening on the same thread in a blocking manner then defining this context would be very simple, it's just any state defined on the thread, whatever variable you define is available to all subsequent operations. In an async scenario this has to be managed in a more sophisticated manner. What we basically have to do is save the request context before starting any async operation and restore it afterwards. Request Context is stored in com.twitter.util.Local.

Local is a utility class built on Java's ThreadLocal that provides operations to easily save and restore data saved in Local onto the current thread. This saving and restoring of state is exactly what happens in async operations in finagle. Async operations are typically implemented as promises. A promise is created at the start of an async operation. This promise implements the future interface and can be passed to callers who can use them to define operations pending on the execution of the underlying async operation of the promise. When the async operation finishes, the promise is marked complete. Thus a promise wraps the execution of an async operation. Finagle does an additional thing here. Before executing the async operation the local context, defined using com.twitter.util.Local, is stored away and after the async operation finishes it is restored. Thus the local context survives the async operation. This facility is extremely powerful as we'll see. We can stow away some piece of data regarding a particular request and have it available in all computations spawned by the request.

Delegations Tables or DTabs are a mechanism of defining how names map to locations where locations are network locations defined ultimately as ipaddress+port. Imagine on your desktop machine you had mounted a remote server's directory, the remote server directory in turn had mounted another server's directory, which had mounted a disk under it's directory somewhere and so on. When you want to read from a file in such a filesystem a lot of resolutions may happen behind the scenes. After some prefix of the path, when a mounted location is encountered, it would have to be mapped to a network location. This kind of a thing will go on until we find the file in question in some sector of some disk somewhere. My point is that names translate to locations. Similarly in Wily naming names translate to locations albeit the locations are host port pairs in the ip universe. There is an elaborate process of resolution to achieve that. Finagle allows overriding the resolution process at various granularities, the finest of which is an individual request. The overrides are defined by things called delegation tables, which basically define prefix substitutions. I won't go into the details of that mechanism here, that's a different topic, but talk about how overriding of these dtabs per request is achieved.

The DTab overrides for a request are basically stored in the local context, defined by com.twitter.util.Local. This dtab override then stays available through out the asynchronous life of a request and affects every network request that this request in turn spawns. Thus every downstream network request that this request triggers uses the overridden delegation table. Well that's not the end of it, for participating protocols such as Http and Thrift this overridden delegation table gets written to outgoing requests and read back on the receiving services. Thus these overrides are basically preserved across the entire network stack for the request. Is this cool or what? Calling it amazing would be an understatement, if you ask me.