Skip to content

Commit e0e8f7e

Browse files
committed
Merge remote-tracking branch 'mrjoes/perf'
2 parents d2457ec + 7485f2a commit e0e8f7e

File tree

5 files changed

+113
-126
lines changed

5 files changed

+113
-126
lines changed

tornado/gen.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def handle_exception(typ, value, tb):
136136
if runner is not None:
137137
return runner.handle_exception(typ, value, tb)
138138
return False
139-
with ExceptionStackContext(handle_exception) as deactivate:
139+
with ExceptionStackContext(handle_exception):
140140
try:
141141
result = func(*args, **kwargs)
142142
except (Return, StopIteration) as e:
@@ -149,15 +149,13 @@ def final_callback(value):
149149
"@gen.engine functions cannot return values: "
150150
"%r" % (value,))
151151
assert value is None
152-
deactivate()
153152
runner = Runner(result, final_callback)
154153
runner.run()
155154
return
156155
if result is not None:
157156
raise ReturnValueIgnoredError(
158157
"@gen.engine functions cannot return values: %r" %
159158
(result,))
160-
deactivate()
161159
# no yield, so we're done
162160
return wrapper
163161

@@ -210,24 +208,21 @@ def handle_exception(typ, value, tb):
210208
typ, value, tb = sys.exc_info()
211209
future.set_exc_info((typ, value, tb))
212210
return True
213-
with ExceptionStackContext(handle_exception) as deactivate:
211+
with ExceptionStackContext(handle_exception):
214212
try:
215213
result = func(*args, **kwargs)
216214
except (Return, StopIteration) as e:
217215
result = getattr(e, 'value', None)
218216
except Exception:
219-
deactivate()
220217
future.set_exc_info(sys.exc_info())
221218
return future
222219
else:
223220
if isinstance(result, types.GeneratorType):
224221
def final_callback(value):
225-
deactivate()
226222
future.set_result(value)
227223
runner = Runner(result, final_callback)
228224
runner.run()
229225
return future
230-
deactivate()
231226
future.set_result(result)
232227
return future
233228
return wrapper

tornado/stack_context.py

Lines changed: 107 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,6 @@ def die_on_error():
6969

7070
from __future__ import absolute_import, division, print_function, with_statement
7171

72-
import contextlib
73-
import functools
74-
import operator
7572
import sys
7673
import threading
7774

@@ -84,7 +81,7 @@ class StackContextInconsistentError(Exception):
8481

8582
class _State(threading.local):
8683
def __init__(self):
87-
self.contexts = ()
84+
self.contexts = (tuple(), None)
8885
_state = _State()
8986

9087

@@ -108,34 +105,41 @@ class StackContext(object):
108105
context that are currently pending). This is an advanced feature
109106
and not necessary in most applications.
110107
"""
111-
def __init__(self, context_factory, _active_cell=None):
108+
def __init__(self, context_factory):
112109
self.context_factory = context_factory
113-
self.active_cell = _active_cell or [True]
110+
self.contexts = []
111+
112+
# StackContext protocol
113+
def enter(self):
114+
context = self.context_factory()
115+
self.contexts.append(context)
116+
context.__enter__()
117+
118+
def exit(self, type, value, traceback):
119+
context = self.contexts.pop()
120+
context.__exit__(type, value, traceback)
114121

115122
# Note that some of this code is duplicated in ExceptionStackContext
116123
# below. ExceptionStackContext is more common and doesn't need
117124
# the full generality of this class.
118125
def __enter__(self):
119126
self.old_contexts = _state.contexts
120-
# _state.contexts is a tuple of (class, arg, active_cell) tuples
121-
self.new_contexts = (self.old_contexts +
122-
((StackContext, self.context_factory,
123-
self.active_cell),))
127+
self.new_contexts = (self.old_contexts[0] + (self,), self)
124128
_state.contexts = self.new_contexts
129+
125130
try:
126-
self.context = self.context_factory()
127-
self.context.__enter__()
128-
except Exception:
131+
self.enter()
132+
except:
129133
_state.contexts = self.old_contexts
130134
raise
131-
return lambda: operator.setitem(self.active_cell, 0, False)
132135

133136
def __exit__(self, type, value, traceback):
134137
try:
135-
return self.context.__exit__(type, value, traceback)
138+
self.exit(type, value, traceback)
136139
finally:
137140
final_contexts = _state.contexts
138141
_state.contexts = self.old_contexts
142+
139143
# Generator coroutines and with-statements with non-local
140144
# effects interact badly. Check here for signs of
141145
# the stack getting out of sync.
@@ -146,7 +150,6 @@ def __exit__(self, type, value, traceback):
146150
raise StackContextInconsistentError(
147151
'stack_context inconsistency (may be caused by yield '
148152
'within a "with StackContext" block)')
149-
self.old_contexts = self.new_contexts = None
150153

151154

152155
class ExceptionStackContext(object):
@@ -162,17 +165,17 @@ class ExceptionStackContext(object):
162165
If the exception handler returns true, the exception will be
163166
consumed and will not be propagated to other exception handlers.
164167
"""
165-
def __init__(self, exception_handler, _active_cell=None):
168+
def __init__(self, exception_handler):
166169
self.exception_handler = exception_handler
167-
self.active_cell = _active_cell or [True]
170+
171+
def exit(self, type, value, traceback):
172+
if type is not None:
173+
return self.exception_handler(type, value, traceback)
168174

169175
def __enter__(self):
170176
self.old_contexts = _state.contexts
171-
self.new_contexts = (self.old_contexts +
172-
((ExceptionStackContext, self.exception_handler,
173-
self.active_cell),))
177+
self.new_contexts = (self.old_contexts[0], self)
174178
_state.contexts = self.new_contexts
175-
return lambda: operator.setitem(self.active_cell, 0, False)
176179

177180
def __exit__(self, type, value, traceback):
178181
try:
@@ -181,11 +184,11 @@ def __exit__(self, type, value, traceback):
181184
finally:
182185
final_contexts = _state.contexts
183186
_state.contexts = self.old_contexts
187+
184188
if final_contexts is not self.new_contexts:
185189
raise StackContextInconsistentError(
186190
'stack_context inconsistency (may be caused by yield '
187191
'within a "with StackContext" block)')
188-
self.old_contexts = self.new_contexts = None
189192

190193

191194
class NullContext(object):
@@ -197,16 +200,12 @@ class NullContext(object):
197200
"""
198201
def __enter__(self):
199202
self.old_contexts = _state.contexts
200-
_state.contexts = ()
203+
_state.contexts = (tuple(), None)
201204

202205
def __exit__(self, type, value, traceback):
203206
_state.contexts = self.old_contexts
204207

205208

206-
class _StackContextWrapper(functools.partial):
207-
pass
208-
209-
210209
def wrap(fn):
211210
"""Returns a callable object that will restore the current `StackContext`
212211
when executed.
@@ -215,64 +214,86 @@ def wrap(fn):
215214
different execution context (either in a different thread or
216215
asynchronously in the same thread).
217216
"""
218-
if fn is None or fn.__class__ is _StackContextWrapper:
217+
# Check if function is already wrapped
218+
if fn is None or hasattr(fn, '_wrapped'):
219219
return fn
220-
# functools.wraps doesn't appear to work on functools.partial objects
221-
#@functools.wraps(fn)
222220

221+
# Capture current stack head
222+
contexts = _state.contexts
223+
224+
#@functools.wraps
223225
def wrapped(*args, **kwargs):
224-
callback, contexts, args = args[0], args[1], args[2:]
225-
226-
if _state.contexts:
227-
new_contexts = [NullContext()]
228-
else:
229-
new_contexts = []
230-
if contexts:
231-
new_contexts.extend(cls(arg, active_cell)
232-
for (cls, arg, active_cell) in contexts
233-
if active_cell[0])
234-
if len(new_contexts) > 1:
235-
with _nested(*new_contexts):
236-
callback(*args, **kwargs)
237-
elif new_contexts:
238-
with new_contexts[0]:
239-
callback(*args, **kwargs)
240-
else:
241-
callback(*args, **kwargs)
242-
return _StackContextWrapper(wrapped, fn, _state.contexts)
243-
244-
245-
@contextlib.contextmanager
246-
def _nested(*managers):
247-
"""Support multiple context managers in a single with-statement.
248-
249-
Copied from the python 2.6 standard library. It's no longer present
250-
in python 3 because the with statement natively supports multiple
251-
context managers, but that doesn't help if the list of context
252-
managers is not known until runtime.
253-
"""
254-
exits = []
255-
vars = []
256-
exc = (None, None, None)
257-
try:
258-
for mgr in managers:
259-
exit = mgr.__exit__
260-
enter = mgr.__enter__
261-
vars.append(enter())
262-
exits.append(exit)
263-
yield vars
264-
except:
265-
exc = sys.exc_info()
266-
finally:
267-
while exits:
268-
exit = exits.pop()
269-
try:
270-
if exit(*exc):
271-
exc = (None, None, None)
272-
except:
273-
exc = sys.exc_info()
274-
if exc != (None, None, None):
275-
# Don't rely on sys.exc_info() still containing
276-
# the right information. Another exception may
277-
# have been raised and caught by an exit method
278-
raise_exc_info(exc)
226+
try:
227+
# Force local state - switch to new stack chain
228+
current_state = _state.contexts
229+
_state.contexts = contexts
230+
231+
# Current exception
232+
exc = (None, None, None)
233+
top = None
234+
235+
# Apply stack contexts
236+
last_ctx = 0
237+
stack = contexts[0]
238+
239+
# Apply state
240+
for n in stack:
241+
try:
242+
n.enter()
243+
last_ctx += 1
244+
except:
245+
# Exception happened. Record exception info and store top-most handler
246+
exc = sys.exc_info()
247+
top = n.old_contexts[1]
248+
249+
# Execute callback if no exception happened while restoring state
250+
if top is None:
251+
try:
252+
fn(*args, **kwargs)
253+
except:
254+
exc = sys.exc_info()
255+
top = contexts[1]
256+
257+
# If there was exception, try to handle it by going through the exception chain
258+
if top is not None:
259+
exc = _handle_exception(top, exc)
260+
else:
261+
# Otherwise take shorter path and run stack contexts in reverse order
262+
while last_ctx > 0:
263+
last_ctx -= 1
264+
c = stack[last_ctx]
265+
266+
try:
267+
c.exit(*exc)
268+
except:
269+
exc = sys.exc_info()
270+
top = c.old_contexts[1]
271+
break
272+
else:
273+
top = None
274+
275+
# If if exception happened while unrolling, take longer exception handler path
276+
if top is not None:
277+
exc = _handle_exception(top, exc)
278+
279+
# If exception was not handled, raise it
280+
if exc != (None, None, None):
281+
raise_exc_info(exc)
282+
finally:
283+
_state.contexts = current_state
284+
285+
wrapped._wrapped = True
286+
return wrapped
287+
288+
289+
def _handle_exception(tail, exc):
290+
while tail is not None:
291+
try:
292+
if tail.exit(*exc):
293+
exc = (None, None, None)
294+
except:
295+
exc = sys.exc_info()
296+
297+
tail = tail.old_contexts[1]
298+
299+
return exc

tornado/test/gen_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,3 +838,6 @@ def test_coroutine_exception_handler(self):
838838
def test_yield_exception_handler(self):
839839
response = self.fetch('/yield_exception')
840840
self.assertEqual(response.body, b'ok')
841+
842+
if __name__ == '__main__':
843+
unittest.main()

tornado/test/stack_context_test.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -95,38 +95,6 @@ def final_callback():
9595
library_function(final_callback)
9696
self.wait()
9797

98-
def test_deactivate(self):
99-
deactivate_callbacks = []
100-
101-
def f1():
102-
with StackContext(functools.partial(self.context, 'c1')) as c1:
103-
deactivate_callbacks.append(c1)
104-
self.io_loop.add_callback(f2)
105-
106-
def f2():
107-
with StackContext(functools.partial(self.context, 'c2')) as c2:
108-
deactivate_callbacks.append(c2)
109-
self.io_loop.add_callback(f3)
110-
111-
def f3():
112-
with StackContext(functools.partial(self.context, 'c3')) as c3:
113-
deactivate_callbacks.append(c3)
114-
self.io_loop.add_callback(f4)
115-
116-
def f4():
117-
self.assertEqual(self.active_contexts, ['c1', 'c2', 'c3'])
118-
deactivate_callbacks[1]()
119-
# deactivating a context doesn't remove it immediately,
120-
# but it will be missing from the next iteration
121-
self.assertEqual(self.active_contexts, ['c1', 'c2', 'c3'])
122-
self.io_loop.add_callback(f5)
123-
124-
def f5():
125-
self.assertEqual(self.active_contexts, ['c1', 'c3'])
126-
self.stop()
127-
self.io_loop.add_callback(f1)
128-
self.wait()
129-
13098
def test_isolation_nonempty(self):
13199
# f2 and f3 are a chain of operations started in context c1.
132100
# f2 is incidentally run under context c2, but that context should

tornado/testing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def get_new_ioloop(self):
169169
return IOLoop()
170170

171171
def _handle_exception(self, typ, value, tb):
172-
self.__failure = sys.exc_info()
172+
self.__failure = (typ, value, tb)
173173
self.stop()
174174
return True
175175

0 commit comments

Comments
 (0)