Categories
Blather Programming Python Software

Deferred All The Way Down

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:

Figure 28: outer and inner deferred processing
Figure 28: outer and inner deferred processing

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:

Figure 29: the thread of control in Figure 28
Figure 29: the thread of control in Figure 28

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

  1. Modify the client so we can ask for a specific kind of transformation by name.
  2. Modify the client so the transformation server address is an optional argument. If it’s not provided, skip the transformation step.
  3. The PoetryClientFactory currently violates the “you make it, you fire it” rule for deferreds. Refactor get_poetry and PoetryClientFactory to remedy that.
  4. 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.
  5. Find the place in the Deferred implementation that handles the case where a callback/errback returns another Deferred.

17 replies on “Deferred All The Way Down”

Hi,
thanks for keeping your work! I have one question: can mfunc below return result from first deferred and just ignore callback_ignore function. I’d like to call callback_ignore and let do work in it in thread without waiting for result and return result from callback_1. What is the way to do it? 1000 Thanks!

from twisted.internet.defer import Deferred
def callback_1(res):
print ‘callback_1 got’, res
return 1

def callback_ignore(res):
print ‘do something’

def mfunc():
d = Deferred()
d.addCallback(callback_1)
d.addCallback(callback_ignore)
d.callback(0)
return d

mfunc()

Ok, so you want to start some work in a thread, but pass along the result from callback_1, right?

In that case I think you want something like this:


def start_work(res):
  deferToThread(my_work_func, res)
  return res # pass the original result along

def mfunc():
  d = Deferred()
  d.addCallback(callback_1)
  d.addCallback(start_work)
  d.callback(0)
  return d

The start_work callback calls deferToThread and then passes the original result down the chain. Makes sense?

Hi dave,
As you said in the chapter,
When meet the inner deferred, the outer deferred will pause.
Until the inner deferred fired, outer deferred will continue.

How Twisted implement the mechanism?
when a callback return a deferred rather than expected value, then outer deferred know that he meet a inner deferred.
He pause and return to reactor.
Then when the inner deferred fired, how to notice the outer deferred continue?

Thanks

My means is,
Twisted will use the returned poem from inner deferred to call the reminder outer callback.
Or Twisted just add the reminder outer callback to inner deferred.

Hey Wind, I see what you are asking. Let’s take a look at the twisted source code for Deferreds here. This is the code where a deferred is running its callback chain. You can see where it tests the result of a callback (or errback ) to see if it is another Deferred (the inner deferred). In that case, the outer deferred pauses itself and adds some callbacks to the inner deferred so that, when the inner deferred fires, the outer deferred will resume running. Does that make sense?

Hi!

you say:
“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.”

Is clientConnectionLost called anyway due to the call of loseConnection in stringReceived?

Thanks!

You did not apply this rule (In general, an object that makes a Deferred should also be in charge of firing that Deferred.) to in the following part:

def get_poetry(host, port):
d = defer.Deferred()
from twisted.internet import reactor
factory = PoetryClientFactory(d)
reactor.connectTCP(host, port, factory)
return d

Should it rather be:

class PoetryClientFactory(ClientFactory):

protocol = PoetryProtocol

def __init__(self):
self.deferred = defer.Deferred()
....

def get_poetry(host, port):
from twisted.internet import reactor
factory = PoetryClientFactory()
reactor.connectTCP(host, port, factory)
return factory.deferred

???

Another thing, is it a common pattern in Twisted that one factory is instantiated for one connection? In this part (https://github.com/jdavisp3/twisted-intro/blob/master/twisted-client-5/get-poetry-1.py#L148) you are creating an instance of a factory in a for loop. I used to associate factories for creating more than one instance of a given object/class and allowing them to share state via the factory.

What if one wanted to request a poem using the same factory? Wouldn’t the self.deferred be already fired for one of these requests? Would it be better for the ClientFactory to instantiate a deferred in buildProtocol for each protocol separately?

Well, I should have read the post more carefully and to the end. So the first question is clear, you actually left it as an exercise to refactor that piece of code.

When it comes to the second question, it did mention that the factory has been designed for a single connection only, however, I’d still like to know if that is so common in Twisted.

Hello! I think in newer versions of Twisted there are APIs for making client connections without a factory. So I believe the answer to your question is that, no, for use cases where a Factory wouldn’t give you any benefit, it would not be necessary or expected to make a Factory.

Leave a Reply to Python Twisted网络编程框架与异步编程入门学习教程(十三)-转载-ClavesCancel reply

Discover more from krondo

Subscribe now to keep reading and get access to the full archive.

Continue reading