Pratchett’s series just gets better and better.
Monthly Archives: March 2010
Book: Bright-sided
An Introduction to Asynchronous Programming and Twisted
Part 13: Deferred All The Way Down
This continues the introduction started here. You can find an index to the entire series here.
Introduction
Recall poetry client 5.1 from Part 10.The client used a Deferred to manage a callback chain that included a call to a poetry transformation engine. In client 5.1, the engine was implemented as a synchronous function call implemented in the client itself.
Now we want to make a new client that uses the networked poetry transformation service we wrote in Part 12. But here’s the wrinkle: since the transformation service is accessed over the network, we’ll need to use asynchronous I/O. And that means our API for requesting a transformation will have to be asynchronous, too. In other words, the
try_to_cummingsify callback is going to return a
Deferred in our new client.
So what happens when a callback in a deferred’s chain returns another deferred? Let’s call the first deferred the ‘outer’ deferred and the second the ‘inner’ one. Suppose callback N in the outer deferred returns the inner deferred. That callback is saying “I’m asynchronous, my result isn’t here yet”. Since the outer deferred needs to call the next callback or errback in the chain with the result, the outer deferred needs to wait until the inner deferred is fired. Of course, the outer deferred can’t block either, so instead the outer deferred suspends the execution of the callback chain and returns control to the reactor (or whatever fired the outer deferred).
And how does the outer deferred know when to resume? Simple — by adding a callback/errback pair to the inner deferred. Thus, when the inner deferred is fired the outer deferred will resume executing its chain. If the inner deferred succeeds (i.e., it calls the callback added by the outer deferred), then the outer deferred calls its N+1 callback with the result. And if the inner deferred fails (calls the errback added by the outer deferred), the outer deferred calls the N+1 errback with the failure.
That’s a lot to digest, so let’s illustrate the idea in Figure 28:
In this figure the outer deferred has 4 layers of callback/errback pairs. When the outer deferred fires, the first callback in the chain returns a deferred (the inner deferred). At that point, the outer deferred will stop firing its chain and return control to the reactor (after adding a callback/errback pair to the inner deferred). Then, some time later, the inner deferred fires and the outer deferred resumes processing its callback chain. Note the outer deferred does not fire the inner deferred itself. That would be impossible, since the outer deferred cannot know when the inner deferred’s result is available, or what that result might be. Rather, the outer deferred simply waits (asynchronously) for the inner deferred to fire.
Notice how the line connecting the callback to the inner deferred in Figure 28 is black instead of green or red. That’s because we don’t know whether the callback succeeded or failed until the inner deferred is fired. Only then can the outer deferred decide whether to call the next callback or the next errback in its own chain.
Figure 29 shows the same outer/inner deferred firing sequence in Figure 28 from the point of view of the reactor:
This is probably the most complicated feature of the
Deferred class, so don’t worry if you need some time to absorb it. We’ll illustrate it one more way using the example code in twisted-deferred/defer-10.py. That example creates two outer deferreds, one with plain callbacks, and one where a single callback returns an inner deferred. By studying the code and the output you can see how the second outer deferred stops running its chain when the inner deferred is returned, and then starts up again when the inner deferred is fired.
Client 6.0
Let’s use our new knowledge of nested deferreds and re-implement our poetry client to use the network transformation service from Part 12. You can find the code in twisted-client-6/get-poetry.py. The poetry Protocol and Factory are unchanged from the previous version. But now we have a Protocol and Factory for making transformation requests. Here’s the transform client Protocol:
class TransformClientProtocol(NetstringReceiver): def connectionMade(self): self.sendRequest(self.factory.xform_name, self.factory.poem) def sendRequest(self, xform_name, poem): self.sendString(xform_name + '.' + poem) def stringReceived(self, s): self.transport.loseConnection() self.poemReceived(s) def poemReceived(self, poem): self.factory.handlePoem(poem)
Using the NetstringReceiver as a base class makes this implementation pretty simple. As soon as the connection is established we send the transform request to the server, retrieving the name of the transform and the poem from our factory. And when we get the poem back, we pass it on to the factory for processing. Here’s the code for the Factory:
class TransformClientFactory(ClientFactory): protocol = TransformClientProtocol def __init__(self, xform_name, poem): self.xform_name = xform_name self.poem = poem self.deferred = defer.Deferred() def handlePoem(self, poem): d, self.deferred = self.deferred, None d.callback(poem) def clientConnectionLost(self, _, reason): if self.deferred is not None: d, self.deferred = self.deferred, None d.errback(reason) clientConnectionFailed = clientConnectionLost
This factory is designed for clients and handles a single transformation request, storing both the transform name and the poem for use by the Protocol. The Factory creates a single Deferred which represents the result of the transformation request. Notice how the Factory handles two error cases: a failure to connect and a connection that is closed before the poem is received. Also note the
clientConnectionLost method is called even if we receive the poem, but in that case
self.deferred will be
None, thanks to the
handlePoem method.
This Factory class creates the Deferred that it also fires. That’s a good rule to follow in Twisted programming, so let’s highlight it:
In general, an object that makes a Deferred should also be in charge of firing that Deferred.
This “you make it, you fire it” rule helps ensure a given deferred is only fired once and makes it easier to follow the flow of control in a Twisted program.
In addition to the transform Factory, there is also a Proxy class which hides the details of making the TCP connection to a particular transform server:
class TransformProxy(object): """ I proxy requests to a transformation service. """ def __init__(self, host, port): self.host = host self.port = port def xform(self, xform_name, poem): factory = TransformClientFactory(xform_name, poem) from twisted.internet import reactor reactor.connectTCP(self.host, self.port, factory) return factory.deferred
This class presents a single
xform() interface that other code can use to request transformations. So that other code can just request a transform and get a deferred back without mucking around with hostnames and port numbers.
The rest of the program is unchanged except for the
try_to_cummingsify callback:
def try_to_cummingsify(poem): d = proxy.xform('cummingsify', poem) def fail(err): print >>sys.stderr, 'Cummingsify failed!' return poem return d.addErrback(fail)
This callback now returns a deferred, but we didn’t have to change the rest of the
main function at all, other than to create the Proxy instance. Since
try_to_cummingsify was part of a deferred chain (the deferred returned by
get_poetry), it was already being used asynchronously and nothing else need change.
You’ll note we are returning the result of
d.addErrback(fail). That’s just a little bit of syntactic sugar. The
addCallback and
addErrback methods return the original deferred. We might just as well have written:
d.addErrback(fail) return d
The first version is the same thing, just shorter.
Testing out the Client
The new client has a slightly different syntax than the others. If you have a transformation service running on port 10001 and two poetry servers running on ports 10002 and 10003, you would run:
python twisted-client-6/get-poetry.py 10001 10002 10003
To download two poems and transform them both. You can start the transform server like this:
python twisted-server-1/transformedpoetry.py --port 10001
And the poetry servers like this:
python twisted-server-1/fastpoetry.py --port 10002 poetry/fascination.txt python twisted-server-1/fastpoetry.py --port 10003 poetry/science.txt
Then you can run the poetry client as above. After that, try crashing the transform server and re-running the client with the same command.
Wrapping Up
In this Part we learned how deferreds can transparently handle other deferreds in a callback chain, and thus we can safely add asynchronous callbacks to an ‘outer’ deferred without worrying about the details. That’s pretty handy since lots of our functions are going to end up being asynchronous.
Do we know everything there is to know about deferreds yet? Not quite! There’s one more important feature to talk about, but we’ll save it for Part 14.
Suggested Exercises
- Modify the client so we can ask for a specific kind of transformation by name.
- Modify the client so the transformation server address is an optional argument. If it’s not provided, skip the transformation step.
- The
PoetryClientFactorycurrently violates the “you make it, you fire it” rule for deferreds. Refactor
get_poetryand
PoetryClientFactoryto remedy that.
- Although we didn’t demonstrate it, the case where an errback returns a deferred is symmetrical. Modify the twisted-deferred/defer-10.py example to verify it.
- Find the place in the Deferred implementation that handles the case where a callback/errback returns another Deferred.
Over and Out Rag
Here’s a rough cut of the only song on the Hanson book I really like.