2 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
3 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
7 <title>Applications of the Twisted Framework</title>
8 <link href="stylesheet.css" type="text/css" rel="stylesheet" />
11 <h1>Applications of the Twisted Framework</h1>
13 <p>exarkun@twistedmatrix.com</p>
17 <p>Two projects developed using the Twisted framework are described;
18 one, Twisted.names, which is included as part of the Twisted
19 distribution, a domain name server and client API, and one, Pynfo, which
20 is packaged separately, a network information robot.</p>
22 <h2>Twisted (dot) Names</h2>
25 <p>The field of domain name servers is well explored and numerous
26 strong, widely-deployed implementations of the protocol exist. DNSSEC,
27 IPv6, service location, geographical location, and many of the other DNS
28 extension proposals all have high quality support in BIND, djbdns,
29 maradns, and others. From a client's perspective, though, the landscape
30 looks a little different. APIs to perform arbitrary domain name lookups
31 are sparse. In contrast, Twisted.names presents a richly featured,
32 asynchronous client API.</p>
35 <p><b>Names</b> is capable of operating as a fully functional domain
36 name server. It implements caching, recursive lookups, and can act as
37 the authority for an arbitrary number of domains. It is not, however, a
38 finely tuned performance machine. Responding to queries can take about
39 twice the time other domain name servers might need. It has not been
40 investigated whether this is a design limitation or merely the result of
41 an unoptimized implementation.</p>
44 <p>As a client, <b>Names</b> provides an easy interface to every type of
45 record supported by. Looking up the MX records for a host, for example,
46 might look like this:</p>
49 def _cbMailExchange(results):
50 # Callback for MX query
52 print 'Mail Exchange is: ', answers
54 def _ebMailExchange(failure):
55 # Error callback for MX query
56 print 'Lookup failed:'
57 failure.printTraceback()
59 from twisted.names import client
60 d = client.lookupMailExchange('example-domain.com')
61 d.addCallbacks(_cbMailExchange, _ebMailExchange)
64 <p>Looking up other record types is as simple as calling a different
65 <code>lookup*</code> function.</p>
67 <h3>Implementation</h3>
69 <p>As with most network software written using Twisted, the first step
70 in developing <b>Names</b> was to write the protocol support. In this
71 case, the protocol was DNS, and support was partially implemented.
72 However, it attempted to merge support for both UDP and TCP, and ended
73 up with less than optimal results. Much of this code was discarded,
74 though some of the lowest level encoding and decoding code worked well
77 <p>With the two protocol classes, DNSDatagramProtocol and DNSProtocol
78 (the TCP version) implemented, the next step was to write classes which
79 created the proper behavior for a domain name server. This logic was
80 put in the <code>twisted.names.server.DNSServerFactory</code> class,
81 which in turn relies on several different kind of <code>Resolver</code>s
82 to find the appropriate response to queries it receives from the
83 protocol instance.</p>
85 <p>The chain of execution, then, is this: a packet is received by the
86 protocol object (a <code>DNSDatagramProtocol</code> or
87 <code>DNSProtocol</code> instance); the packet is decoded by
88 <code>twisted.protocols.dns.RRHeader</code> in cooperation with one of
89 the record classes (<code>twisted.protocols.dns.Record_A</code> for
90 example); the decoded <code>twisted.protocols.dns.Query</code> object is
91 passed up to the <code>twisted.names.server.DNSServerFactory</code>,
92 which determines the query type and invokes the appropriate lookup
93 method on each of its resolver objects in turn; if an answer is found,
94 it is passed back down to the protocol instance (otherwise the
95 appropriate bit for an error condition is set), where it is encoded and
96 transmitted back to the client.</p>
98 <p>There are four kinds of resolvers in the current implementation. The
99 first three are authorities, caches, and recursive resolvers. They are
100 generally queried, in this order, using the fourth resolver, the "chain"
101 resolver, which simply queries the resolvers it knows about, moving on
102 to the next when any given resolver fails to produce a response, and
103 generating the proper exception when the last resolver has failed.</p>
105 <h3>Shortcomings</h3>
107 <p>There are several aspects of Twisted Names that might preclude its
108 use in "production" software. These issues stem mainly from its
109 immaturity, it being less than six months old at the writing of this
113 <li><p>Possibly of foremost interest to those who might use it in a
114 high-load environment, it has somewhat poor runtime performance
115 characteristics. One potential reason for this is the extensive use of
116 exceptions to signal the relatively common case of a resolver lookup
117 failing. Solutions to this problem are apparent, but an implementation
118 change has not been attempted. Until this area of its development is
119 more fully examined, it will likely not be of use in anything other than
120 for low- to mid-load tasks, or with more hardware available to it than
121 might seem reasonable.</p></li>
123 <li>No attempt has been made to implement DNSSEC.</li>
125 <li>Certain areas of the server remain out of compliance with the
126 standardized RFCs, occasionally causing undesirable behavior when
127 interacting with clients. This most frequently manifests itself as a
128 lookup which fails the first time and succeeds on subsequent attempts.
129 It is not believed that these represent architectural flaws, only small
130 oversights in areas such as the "additional processing" sections of the
131 current authority resolver implementations.</li>
136 <p>Pynfo was originally begun as a learning project to become acquainted
137 with the Twisted framework. After a brief initial development period
138 and an extended period of non-development, Pynfo was picked up again to
139 serve as a replacement for several existing robots, each with fragile
140 code bases and with designs not intended for future integration with
141 other services. After it subsumed the functions of network relaying and
142 Google searches, other desired features, which enhanced the IRC medium
143 and had not previously been considered due to the difficulty of
144 extending existing robots, were added to Pynfo, prompting the development
145 of an elementary plug-in system to further facilitate the integration
148 <h3>Architecture</h3>
149 <p>Pynfo performs such simple tasks as noting the last time an
150 individual spoke and querying the Google search engine, as well as
151 several more complex operations like relaying traffic between different
152 IRC networks and publishing channel logs through an HTTP interface.</p>
154 <p>Toward these ends, it is useful to abstract the functionality into
155 several different layers:</p>
158 <li><p>The factory: All shared data, such as the channels a given user is
159 known to be in, the plugins currently loaded, and the addresses of servers
160 to connect to, is aggregated here. When it is necessary to make a
161 connection, the factory creates an instance of the appropriate Protocol
162 subclass, in a manner similar to this:
165 def buildProtocol(self, address):
166 for net in self.data['networks'].values():
167 if net.address == address:
170 proto = IRCProtocol(net)
171 self.allBots[net.alias] = proto
176 The factory instance is created only once, and that instance persists
177 through the entire time a particular Pynfo bot operates.</p>
180 <li><p>The protocol: Each kind of service Pynfo can connect to has a
181 Protocol class associated with it, a class which handles the specifics
182 of communicating over this protocol. Unlike the factory, protocols
183 instances can be short lived and are created and destroyed as many times
184 as network connectivity demands. When a Pynfo robot shuts down and is
185 serialized to disk, all Protocol instances are destroyed and discarded,
186 to be created anew when the robot is restarted.</p>
189 <li><p>Plugins: These give Pynfo most of its functionality. From the
190 very simple logging module, which does no more than write strings to
191 disk, to the esoteric lookup module, which translates hostnames into
192 dotted-quads, to the informative dictionary module, which queries an <a
193 href="http://dict.org">online dictionary</a>, plugins come in all shapes
194 and sizes, and can be written to fill almost any niche.</p>
198 <h3>Employing Components</h3>
199 <p>Twisted provides a <i>component</i> system which Pynfo relies on to
200 split up useful functionality used in different areas of the code. The
201 Interface class is the primary element in the component system, and is
202 used as a location for a semi-format definition of an API, as well as
203 documentation. Classes declare that they implement an Interface by
204 including it in their __implements__ tuple attribute. Interfaces can
205 also be added to classes by third parties using the registerAdapter()
206 function. This takes an Adapter type in addition to the interface being
207 registered and the type it is being registered for. Adapters are a
208 objects which can store additional state information and implement
209 functionality without being part of the classes that are "actually"
210 being operated upon. They, as their name suggests, adapt components to
211 conform to interfaces.</p>
213 <p>Components can implement interfaces themselves, or maintain a cache
214 of adapter objects for each interfaces that is requested of them. These
215 persist like any other attribute, and so state stored in adapters
216 remains associated with the component as long as that component exists, or
217 until the adapter is explicitly removed.</p>
219 <p>Pynfo's Factory class uses two adapters to implement two basic
220 Interfaces that many plugins find useful. The first is the IStorage
224 class IStorage(components.Interface):
226 def store(self, key, version, value):
228 Store any pickleable object
231 def retrieve(self, key, version):
233 Retrieve the previously stored object associated with key and
237 An example usage of this interface is the PyPI plugin, which polls the
238 Python Package Index and reports updates to a configurable list of
243 global notifyChannels
244 store = factory.getComponent(interfaces.IStorage)
246 notifyChannels = store.retrieve('pypi', __version__)
247 except error.RetrievalError:
251 <p>The module requests the component of factory which implements
252 IStorage, then attempts to load any previously stored version of
253 "notifyChannels". If none is found, it defaults to none. In the
254 finalizer below, this global is stored, using the same interfaced, to be
255 retrieved when the module is next initialized.</p>
259 s = factory.getComponent(interfaces.IStorage)
260 s.store('pypi', __version__, notifyChannels)
263 The second interface allows low granularity scheduling of events:
266 class IScheduler(components.Interface):
270 WEEKLY = 60 * 60 * 24 * 7
273 def schedule(self, period, fn, *args, **kw):
275 Cause a function to be invoked at regular intervals with the given
279 The Adapter which implements this interface is just as simple:
281 class SchedulerAdapter(components.Adapter):
282 __implements__ = (interfaces.IScheduler,)
284 def schedule(self, period, fn, *args, **kw):
285 from twisted.internet import reactor
288 reactor.callLater(period, cycle)
289 reactor.callLater(period, cycle)
293 <p>Implementing these interfaces as adapters using the component system
294 has two primary advantages over a simple inheritance or mixins approach.
295 First, it allows plugins to add completely new behavior to the system
296 without complex and fragile manipulation of the factory's __class__
297 attribute. This is a big win when it comes to plugins that want to
298 share new functionality with other plugins. For example, the "ignore"
299 plugin adds an IDiscriminating interface and an adapter which implements
300 it. Once this plugin is loaded, any other plugin can request the
301 component for IDiscriminating and add users to or remove users from the
304 <h3>The Plugin Framework</h3>
306 <p>Before a module can be loaded and initialized as a plugin, it must be
307 located. This could be done with a simple use of
308 <code>os.listdir()</code>, or <code>__all__</code> could be set to include
309 each new plugin added. Twisted provides another way, though.</p>
311 <p>The <code>twisted.python.plugin</code> provides the most high-level
312 interface to the plugin system, a function called
313 <code>getPlugIns</code>. It usually takes one argument, a plugin type,
314 which is an arbitrary string used to categorize the different kinds of
315 plugins available on a system. Twisted's own "mktap" tool uses the
316 "tap" plugin type. For Pynfo, I have elected to use the "infobot"
317 string. <code>getPlugIns("infobot")</code> searches the system (by way
318 of PYTHONPATH) for files named "plugins.tml". These files contain
319 python source, and are run as such; a function, "register" is placed in
320 their namespace, and the most common action for them is to invoke this
321 function one or more times, providing information about a plugin. Here
322 is a snippet from one which Pynfo uses:</p>
327 "Pynfo.plugins.weather",
328 description="Commands to check the weather at "
329 "various places around the world.",
334 <p>Any number of plugin.tml files may exist in the filesystem, allowing
335 per-user and even per-robot plugins to be installed, all without
336 modifying the Pynfo installation itself.
338 The second argument indicates the module which may be imported to get
339 this plugin. Pynfo traverses the resulting list, importing these modules,
340 and initializing them if necessary.</p>