Sponsored Link •
For some time now, my intuition has been telling me that the Twisted Matrix is something that would be good to understand. Maybe it's just from seeing the kind of energy that the Twisted guys exhibit at Python conferences.
The problem is that the terminology they use has never made much sense to me. It seems like they make up words that seem sensible to them at the moment, but that I haven't been able to make heads or tails of.
I've been studying distributed systems, things like XML-RPC. XML-RPC is nice when you are making calls across the network and you don't want to worry about how the server is implemented. As long as the server speaks XML-RPC (or SOAP, if you must -- but from what I've heard, web services have mostly been providing XML-RPC as an alternative when they offer SOAP, and that the vast majority of people are using XML-RPC), you don't care what language that server was written in.
But what if you have control over both ends? XML-RPC still works fine, but if you're programming in Java, maybe it makes more sense to use RMI. OK, RMI might end up being more complicated than XML-RPC, so consider a distributed system where all the clients and servers are using Python.
Python has a perfectly fine XML-RPC system, and that makes sense as long as you're doing synchronous calls -- that is, you make a call and expect the result to be returned right away (this is also what Java RMI expects). But in a distributed system it's not uncommon to make asynchronous calls, where you make the call and it takes awhile to complete the result, so you don't want the caller to be stopped while waiting for the result. Instead, what you'd like is for the call to return, so the caller can go on and do something else, and then sometime later, when the server finishes the request, it can somehow transport the result back to the client. And this is where things get tricky, and where simple synchronous systems like XML-RPC or Java RMI don't serve so well.
If you've studied the concurrency library in Java 5 (and this amounts to the longest chapter in Thinking in Java 4th edition), you'll know that a new construct has been introduced -- new to Java, not to the programming community at large -- the Future. A Future is an interface that is returned from an asynchronous call. You can call isDone() to discover whether the underlying call is completed, and get() to get the result. This is fairly primitive, because it requires polling to see whether the call is completed, but it does allow the calling process to continue doing other things rather than being stopped with a synchronous call.
This is where Twisted comes in handy -- it will apparently do a lot of other things, but this is the first feature that I've really understood. And I understood it not from the O'Reilly Twisted book, but rather from the Python Cookbook, 2nd edition, where they have a small, succinct example and short, clear explanation (I find the Python Cookbook one of the best references once you've mastered the basics -- for which I still like Learning Python -- and I also get a lot of use from Python in a Nutshell. And although it's a bit dated now, the Python Standard Library is also full of great examples).
In recipe 15.7, Using Twisted Perspective Broker, they create a server in 11 lines, by using the Twisted Perspective Broker. And here's where I have terminology problems again -- "Perspective Broker" doesn't say to me "thing that you make a network server from," in fact, I don't know what it's supposed to mean. But what you can do with it is indeed very cool. I'm going to try to make an even simpler example here than they showed, because I'm thinking about doing distributed computing in order to distribute long-running processes among machines, just to get the work done faster. So my example will simply be a server that provides a remote method call that takes awhile to complete:
"Server.py: Provides a calculation service across the network" from twisted.spread import pb from twisted.internet import reactor import time PORT = 8992 COUNT = 3 class Calculator(pb.Root): def __init__(self, id): self.id = id print "Calculator", self.id, "running" def remote_calculate(self, a, b): print "Calculator", self.id, "calculating ..." time.sleep(1) return a + b, self.id for i, port in enumerate(range(PORT, PORT + COUNT)): print "port:", port reactor.listenTCP(port, pb.PBServerFactory(Calculator(i))) reactor.run()
The Calculator class provides a remote method called calculate(), which adds its arguments. The call to sleep() simulates a complex, time-consuming task (and we distribute these tasks across machines to get the job done faster).
To create a server that provides this service, you simply inherit your service class from pb.Root and then pass an instance of that class to pb.PBServerFactory(), the result of which is bound to and listens at a particular TCP port, using reactor.listenTCP(). Because other parts of Twisted are more low-level, I think that the goal of the perspective-broker framework was to make this process of creating servers and clients very easy.
The reactor is another one of those words that they use, although I think this one makes sense (it's apparently based on the Reactor Design Pattern, which is not one of the Gang Of Four patterns). It's the object that implements the event loop, and it "reacts" to events (in this case, method calls across the network). This is one of the core elements of Twisted, and they build a lot of stuff on it. But in the above code, we can wave our hands over the code a little bit and say "if you inherit a class from pb.Root and give that class methods that begin with remote_, we can tell a reactor to listen at a TCP port and hand our class to the pb.PBServerFactory constructor, and this will provide our methods as remote network calls. That is, we can create a server with almost no effort.
In order for the client to make an asynchronous call, the call must return right away using a mechanism like Java 5's Future -- which is really just a proxy to a result that will appear sometime in the future. Twisted, however, returns an object that is more clever than a Future, called a Deferred. But instead of polling that Deferred as you do in Java, you add callbacks -- one to call back if the call succeeds, and on to call back if the call fails (of course, failure might not be detectable by the server in some cases, so it's easy to add things like watchdog timers in Twisted).
Because of the possibility of failure when making calls across the network, the client tends to be more complex (and in this example I haven't covered all the possible failure points, in the interest of keeping things simple):
"Client.py: Uses the calculation service across the network" from twisted.spread import pb from twisted.internet import reactor PORT = 8992 COUNT = 3 class ModelCalculator: def __init__(self, host): self.a = 42 self.b = 47 self.count = 0 self.total = 0 print 'Connecting to Servers...' for port in range(PORT, PORT + COUNT): factory = pb.PBClientFactory() reactor.connectTCP(host, port, factory) factory.getRootObject()\ .addCallbacks(self.connected, self.failure) def connected(self, perspective): perspective.callRemote('calculate', self.a, self.b)\ .addCallbacks(self.success, self.failure) print "connected" self.a += 10 self.b += 11 def success(self, result): print result self.total += result self.count += 1 # Alternative: use a DeferredList if(self.count == COUNT): print "total:", self.total reactor.stop() def failure(self, _): print "remote failure" reactor.stop() ModelCalculator("127.0.0.1") reactor.run()
This class distributes calculations to three different servers, then collects the results. To connect to a server, you create a pb.PBClientFactory(), pass it in to reactor.connectTCP(), then get the root object of that factory, which is a defferred representing the connection process. To this, we add "success" and "failure" callbacks, for when the connection process either finishes or fails. Assuming it's sucessful, the connected() method will be called, handing it the perspective object. Now that you have a live connection, you can make a remote call using callRemote(). Again, this returns a deferred object (because it's an asynchronous call), and you can add callbacks indicating success and failure for that call. In the example above, the success() method keeps track of the callbacks it receives so that it can print the final result and stop the reactor when the calculation is complete.
Twisted has lots of other features that I haven't fathomed yet; the book "Twisted Network Programming Essentials" by Abe Fettig (O'Reilly, 2006) is the definitive introduction to Twisted and takes you through those other features. For example, it's quite simple to create an XML-RPC server using Twisted. In the above example, you could factor the calculation (business logic) into a separate class, then expose the functionality of that class using both a perspective broker and as an XML-RPC service -- although the client for the XML-RPC service would then have to deal with the fact that the synchronous calls would not return right away.
|Bruce Eckel (www.BruceEckel.com) provides development assistance in Python with user interfaces in Flex. He is the author of Thinking in Java (Prentice-Hall, 1998, 2nd Edition, 2000, 3rd Edition, 2003, 4th Edition, 2005), the Hands-On Java Seminar CD ROM (available on the Web site), Thinking in C++ (PH 1995; 2nd edition 2000, Volume 2 with Chuck Allison, 2003), C++ Inside & Out (Osborne/McGraw-Hill 1993), among others. He's given hundreds of presentations throughout the world, published over 150 articles in numerous magazines, was a founding member of the ANSI/ISO C++ committee and speaks regularly at conferences.|