Search code examples
pythontwisteddeferredasynchronous-messaging-protocol

Chaining deferred callbacks


I am trying to chain deferreds in AMP client like following:

Client:

from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol
from twisted.protocols.amp import AMP

import commands

def connect_protocol(host, port):
    destination = TCP4ClientEndpoint(reactor, host, port)
    d = connectProtocol(destination, AMP())

    def connect(protocol):
        print 'Connecting to server as Mr Spaceman...'
        return protocol.callRemote(commands.Connect,
                                   username='Mr Foo')

    def say(protocol):
        print 'Saying "Hello world" to the server...'
        return protocol.callRemote(commands.Say,
                                   phrase='Hello world')

    d.addCallback(connect)
    d.addCallback(say)


def main(host, port):
    connect_protocol(host, port)
    print 'Connected to %s:%d...' % (host, port)
    reactor.run()

main('127.0.0.1', 12345)

Server:

from twisted.internet.protocol import Factory
from twisted.protocols.amp import AMP

import commands

class CommandProtocol(AMP):

    def connect(self, username):
        print "Received connect command: %s." % (username)
        return {}
    commands.Connect.responder(connect)

    def say(self, phrase):
        print "Received phrase \"%s\"." % phrase
        return {}
    commands.Say.responder(say)

def main(port):
    factory = Factory()
    factory.protocol = CommandProtocol
    reactor.listenTCP(port, factory)
    print 'Started AMP server on port %d...' % port
    reactor.run()

main(12345)

Only connect() is being fired on server side


Solution

  • First, enable logging:

    from sys import stdout
    from twisted.python.log import startLogging
    startLogging(stdout)
    

    Now you'll see what's going on in the program.

    Second, at least have a final errback that logs unhandled failures on a Deferred so these will show up deterministically rather than depending on the garbage collector:

    from twisted.python.log import err
    
    ...
    
        d.addCallback(connect)
        d.addCallback(say)
        d.addErrback(err, "connect_protocol encountered some problem")
    

    Finally, the result of a Deferred is changed by callbacks and errbacks attached to it. In this case, the argument passed to say is the result of the Deferred returned by connect. This will not be the same as the argument to connect, so it's not likely you can use callRemote on it.

    You can fix this in many different ways. One way that involves minimal code changes (but isn't necessarily the best solution) is to pass the protocol as an extra value in the result of the connect Deferred:

    def connect(protocol):
        print 'Connecting to server as Mr Spaceman...'
        d = protocol.callRemote(commands.Connect, username='Mr Foo')
        d.addCallback(lambda result: (protocol, result))
        return d
    
    def say((protocol, result)):
        print 'Saying "Hello world" to the server...'
        return protocol.callRemote(commands.Say,
                                   phrase='Hello world')