Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 321 lines (259 sloc) 11.014 kb
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
1 """``tornado.gen`` is a generator-based interface to make it easier to
2 work in an asynchronous environment. Code using the ``gen`` module
3 is technically asynchronous, but it is written as a single generator
4 instead of a collection of separate functions.
5
6 For example, the following asynchronous handler::
7
8 class AsyncHandler(RequestHandler):
9 @asynchronous
10 def get(self):
11 http_client = AsyncHTTPClient()
12 http_client.fetch("http://example.com",
13 callback=self.on_fetch)
14
15 def on_fetch(self, response):
16 do_something_with_response(response)
17 self.render("template.html")
18
19 could be written with ``gen`` as::
20
21 class GenAsyncHandler(RequestHandler):
22 @asynchronous
23 @gen.engine
24 def get(self):
25 http_client = AsyncHTTPClient()
93a0dc8 @bdarnell Fix error in docs
bdarnell authored
26 response = yield gen.Task(http_client.fetch, "http://example.com")
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
27 do_something_with_response(response)
28 self.render("template.html")
29
30 `Task` works with any function that takes a ``callback`` keyword argument
94b483e @bdarnell Add support for lists of YieldPoints in the gen framework.
bdarnell authored
31 (and runs that callback with zero or one arguments). You can also yield
32 a list of ``Tasks``, which will be started at the same time and run in parallel;
33 a list of results will be returned when they are all finished::
34
35 def get(self):
36 http_client = AsyncHTTPClient()
37 response1, response2 = yield [gen.Task(http_client.fetch, url1),
38 gen.Task(http_client.fetch, url2)]
39
40 For more complicated interfaces, `Task` can be split into two parts:
41 `Callback` and `Wait`::
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
42
43 class GenAsyncHandler2(RequestHandler):
44 @asynchronous
45 @gen.engine
46 def get(self):
47 http_client = AsyncHTTPClient()
48 http_client.fetch("http://example.com",
49 callback=(yield gen.Callback("key"))
50 response = yield gen.Wait("key")
51 do_something_with_response(response)
52 self.render("template.html")
53
54 The ``key`` argument to `Callback` and `Wait` allows for multiple
94b483e @bdarnell Add support for lists of YieldPoints in the gen framework.
bdarnell authored
55 asynchronous operations to be started at different times and proceed
56 in parallel: yield several callbacks with different keys, then wait
57 for them once all the async operations have started.
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
58 """
59
60 import functools
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
61 import sys
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
62 import types
63
64 class KeyReuseError(Exception): pass
65 class UnknownKeyError(Exception): pass
66 class LeakedCallbackError(Exception): pass
67 class BadYieldError(Exception): pass
68
69 def engine(func):
70 """Decorator for asynchronous generators.
71
72 Any generator that yields objects from this module must be wrapped
73 in this decorator. The decorator only works on functions that are
74 already asynchronous. For `~tornado.web.RequestHandler`
75 ``get``/``post``/etc methods, this means that both the `tornado.gen.engine`
76 and `tornado.web.asynchronous` decorators must be used (in either order).
77 In most other cases, it means that it doesn't make sense to use
78 ``gen.engine`` on functions that don't already take a callback argument.
79 """
80 @functools.wraps(func)
81 def wrapper(*args, **kwargs):
82 gen = func(*args, **kwargs)
83 if isinstance(gen, types.GeneratorType):
84 Runner(gen).run()
85 return
86 assert gen is None, gen
87 # no yield, so we're done
88 return wrapper
89
90 class YieldPoint(object):
91 """Base class for objects that may be yielded from the generator."""
92 def start(self, runner):
93 """Called by the runner after the generator has yielded.
94
95 No other methods will be called on this object before ``start``.
96 """
97 raise NotImplementedError()
98
99 def is_ready(self):
100 """Called by the runner to determine whether to resume the generator.
101
2f91460 @bdarnell Allow any sequence in gen.WaitAll, not just lists.
bdarnell authored
102 Returns a boolean; may be called more than once.
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
103 """
104 raise NotImplementedError()
105
106 def get_result(self):
107 """Returns the value to use as the result of the yield expression.
108
109 This method will only be called once, and only after `is_ready`
110 has returned true.
111 """
112 raise NotImplementedError()
113
114 class Callback(YieldPoint):
115 """Returns a callable object that will allow a matching `Wait` to proceed.
116
117 The key may be any value suitable for use as a dictionary key, and is
118 used to match ``Callbacks`` to their corresponding ``Waits``. The key
119 must be unique among outstanding callbacks within a single run of the
120 generator function, but may be reused across different runs of the same
121 function (so constants generally work fine).
122
123 The callback may be called with zero or one arguments; if an argument
124 is given it will be returned by `Wait`.
125 """
126 def __init__(self, key):
127 self.key = key
128
129 def start(self, runner):
130 self.runner = runner
131 runner.register_callback(self.key)
132
133 def is_ready(self):
134 return True
135
136 def get_result(self):
137 return self.callback
138
139 def callback(self, arg=None):
140 self.runner.set_result(self.key, arg)
141
142 class Wait(YieldPoint):
143 """Returns the argument passed to the result of a previous `Callback`."""
144 def __init__(self, key):
145 self.key = key
146
147 def start(self, runner):
148 self.runner = runner
149
150 def is_ready(self):
151 return self.runner.is_ready(self.key)
152
153 def get_result(self):
154 return self.runner.pop_result(self.key)
155
0965952 @bdarnell Add gen.WaitAll
bdarnell authored
156 class WaitAll(YieldPoint):
157 """Returns the results of multiple previous `Callbacks`.
158
159 The argument is a sequence of `Callback` keys, and the result is
160 a list of results in the same order.
94b483e @bdarnell Add support for lists of YieldPoints in the gen framework.
bdarnell authored
161
162 `WaitAll` is equivalent to yielding a list of `Wait` objects.
0965952 @bdarnell Add gen.WaitAll
bdarnell authored
163 """
164 def __init__(self, keys):
165 self.keys = keys
166
167 def start(self, runner):
168 self.runner = runner
169
170 def is_ready(self):
171 return all(self.runner.is_ready(key) for key in self.keys)
172
173 def get_result(self):
174 return [self.runner.pop_result(key) for key in self.keys]
175
176
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
177 class Task(YieldPoint):
178 """Runs a single asynchronous operation.
179
180 Takes a function (and optional additional arguments) and runs it with
181 those arguments plus a ``callback`` keyword argument. The argument passed
182 to the callback is returned as the result of the yield expression.
183
184 A `Task` is equivalent to a `Callback`/`Wait` pair (with a unique
185 key generated automatically)::
186
187 result = yield gen.Task(func, args)
188
189 func(args, callback=(yield gen.Callback(key)))
190 result = yield gen.Wait(key)
191 """
192 def __init__(self, func, *args, **kwargs):
193 assert "callback" not in kwargs
194 kwargs["callback"] = self.callback
195 self.func = functools.partial(func, *args, **kwargs)
196
197 def start(self, runner):
198 self.runner = runner
199 self.key = object()
200 runner.register_callback(self.key)
201 self.func()
202
203 def is_ready(self):
204 return self.runner.is_ready(self.key)
205
206 def get_result(self):
207 return self.runner.pop_result(self.key)
208
209 def callback(self, arg=None):
210 self.runner.set_result(self.key, arg)
211
94b483e @bdarnell Add support for lists of YieldPoints in the gen framework.
bdarnell authored
212 class Multi(YieldPoint):
213 """Runs multiple asynchronous operations in parallel.
214
215 Takes a list of ``Tasks`` or other ``YieldPoints`` and returns a list of
216 their responses. It is not necessary to call `Multi` explicitly,
217 since the engine will do so automatically when the generator yields
218 a list of ``YieldPoints``.
219 """
220 def __init__(self, children):
221 assert all(isinstance(i, YieldPoint) for i in children)
222 self.children = children
223
224 def start(self, runner):
225 for i in self.children:
226 i.start(runner)
227
228 def is_ready(self):
229 return all(i.is_ready() for i in self.children)
230
231 def get_result(self):
232 return [i.get_result() for i in self.children]
233
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
234 class _NullYieldPoint(YieldPoint):
235 def start(self, runner):
236 pass
237 def is_ready(self):
238 return True
239 def get_result(self):
240 return None
241
242 class Runner(object):
243 """Internal implementation of `tornado.gen.engine`.
244
245 Maintains information about pending callbacks and their results.
246 """
247 def __init__(self, gen):
248 self.gen = gen
249 self.yield_point = _NullYieldPoint()
250 self.pending_callbacks = set()
251 self.results = {}
252 self.running = False
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
253 self.finished = False
254 self.exc_info = None
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
255
256 def register_callback(self, key):
257 """Adds ``key`` to the list of callbacks."""
258 if key in self.pending_callbacks:
259 raise KeyReuseError("key %r is already pending" % key)
260 self.pending_callbacks.add(key)
261
262 def is_ready(self, key):
263 """Returns true if a result is available for ``key``."""
264 if key not in self.pending_callbacks:
265 raise UnknownKeyError("key %r is not pending" % key)
266 return key in self.results
267
268 def set_result(self, key, result):
269 """Sets the result for ``key`` and attempts to resume the generator."""
270 self.results[key] = result
271 self.run()
272
273 def pop_result(self, key):
274 """Returns the result for ``key`` and unregisters it."""
275 self.pending_callbacks.remove(key)
276 return self.results.pop(key)
277
278 def run(self):
279 """Starts or resumes the generator, running until it reaches a
280 yield point that is not ready.
281 """
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
282 if self.running or self.finished:
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
283 return
284 try:
285 self.running = True
286 while True:
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
287 if self.exc_info is None:
288 try:
289 if not self.yield_point.is_ready():
290 return
291 next = self.yield_point.get_result()
292 except Exception:
293 self.exc_info = sys.exc_info()
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
294 try:
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
295 if self.exc_info is not None:
296 exc_info = self.exc_info
297 self.exc_info = None
298 yielded = self.gen.throw(*exc_info)
299 else:
300 yielded = self.gen.send(next)
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
301 except StopIteration:
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
302 self.finished = True
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
303 if self.pending_callbacks:
304 raise LeakedCallbackError(
305 "finished without waiting for callbacks %r" %
306 self.pending_callbacks)
307 return
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
308 except Exception:
309 self.finished = True
310 raise
94b483e @bdarnell Add support for lists of YieldPoints in the gen framework.
bdarnell authored
311 if isinstance(yielded, list):
312 yielded = Multi(yielded)
5997f41 @bdarnell Improve exception handling for gen module.
bdarnell authored
313 if isinstance(yielded, YieldPoint):
314 self.yield_point = yielded
315 self.yield_point.start(self)
316 else:
317 self.exc_info = (BadYieldError("yielded unknown object %r" % yielded),)
e4ead59 @bdarnell Add tornado.gen module for simpler generator-based async code.
bdarnell authored
318 finally:
319 self.running = False
320
Something went wrong with that request. Please try again.