00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017 '''StackContext allows applications to maintain threadlocal-like state
00018 that follows execution as it moves to other execution contexts.
00019
00020 The motivating examples are to eliminate the need for explicit
00021 async_callback wrappers (as in tornado.web.RequestHandler), and to
00022 allow some additional context to be kept for logging.
00023
00024 This is slightly magic, but it's an extension of the idea that an exception
00025 handler is a kind of stack-local state and when that stack is suspended
00026 and resumed in a new context that state needs to be preserved. StackContext
00027 shifts the burden of restoring that state from each call site (e.g.
00028 wrapping each AsyncHTTPClient callback in async_callback) to the mechanisms
00029 that transfer control from one context to another (e.g. AsyncHTTPClient
00030 itself, IOLoop, thread pools, etc).
00031
00032 Example usage::
00033
00034 @contextlib.contextmanager
00035 def die_on_error():
00036 try:
00037 yield
00038 except Exception:
00039 logging.error("exception in asynchronous operation",exc_info=True)
00040 sys.exit(1)
00041
00042 with StackContext(die_on_error):
00043 # Any exception thrown here *or in callback and its desendents*
00044 # will cause the process to exit instead of spinning endlessly
00045 # in the ioloop.
00046 http_client.fetch(url, callback)
00047 ioloop.start()
00048
00049 Most applications shouln't have to work with `StackContext` directly.
00050 Here are a few rules of thumb for when it's necessary:
00051
00052 * If you're writing an asynchronous library that doesn't rely on a
00053 stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
00054 (for example, if you're writing a thread pool), use
00055 `stack_context.wrap()` before any asynchronous operations to capture the
00056 stack context from where the operation was started.
00057
00058 * If you're writing an asynchronous library that has some shared
00059 resources (such as a connection pool), create those shared resources
00060 within a ``with stack_context.NullContext():`` block. This will prevent
00061 ``StackContexts`` from leaking from one request to another.
00062
00063 * If you want to write something like an exception handler that will
00064 persist across asynchronous calls, create a new `StackContext` (or
00065 `ExceptionStackContext`), and make your asynchronous calls in a ``with``
00066 block that references your `StackContext`.
00067 '''
00068
00069 from __future__ import absolute_import, division, with_statement
00070
00071 import contextlib
00072 import functools
00073 import itertools
00074 import operator
00075 import sys
00076 import threading
00077
00078 from tornado.util import raise_exc_info
00079
00080
00081 class _State(threading.local):
00082 def __init__(self):
00083 self.contexts = ()
00084 _state = _State()
00085
00086
00087 class StackContext(object):
00088 '''Establishes the given context as a StackContext that will be transferred.
00089
00090 Note that the parameter is a callable that returns a context
00091 manager, not the context itself. That is, where for a
00092 non-transferable context manager you would say::
00093
00094 with my_context():
00095
00096 StackContext takes the function itself rather than its result::
00097
00098 with StackContext(my_context):
00099
00100 The result of ``with StackContext() as cb:`` is a deactivation
00101 callback. Run this callback when the StackContext is no longer
00102 needed to ensure that it is not propagated any further (note that
00103 deactivating a context does not affect any instances of that
00104 context that are currently pending). This is an advanced feature
00105 and not necessary in most applications.
00106 '''
00107 def __init__(self, context_factory, _active_cell=None):
00108 self.context_factory = context_factory
00109 self.active_cell = _active_cell or [True]
00110
00111
00112
00113
00114 def __enter__(self):
00115 self.old_contexts = _state.contexts
00116
00117 _state.contexts = (self.old_contexts +
00118 ((StackContext, self.context_factory, self.active_cell),))
00119 try:
00120 self.context = self.context_factory()
00121 self.context.__enter__()
00122 except Exception:
00123 _state.contexts = self.old_contexts
00124 raise
00125 return lambda: operator.setitem(self.active_cell, 0, False)
00126
00127 def __exit__(self, type, value, traceback):
00128 try:
00129 return self.context.__exit__(type, value, traceback)
00130 finally:
00131 _state.contexts = self.old_contexts
00132
00133
00134 class ExceptionStackContext(object):
00135 '''Specialization of StackContext for exception handling.
00136
00137 The supplied exception_handler function will be called in the
00138 event of an uncaught exception in this context. The semantics are
00139 similar to a try/finally clause, and intended use cases are to log
00140 an error, close a socket, or similar cleanup actions. The
00141 exc_info triple (type, value, traceback) will be passed to the
00142 exception_handler function.
00143
00144 If the exception handler returns true, the exception will be
00145 consumed and will not be propagated to other exception handlers.
00146 '''
00147 def __init__(self, exception_handler, _active_cell=None):
00148 self.exception_handler = exception_handler
00149 self.active_cell = _active_cell or [True]
00150
00151 def __enter__(self):
00152 self.old_contexts = _state.contexts
00153 _state.contexts = (self.old_contexts +
00154 ((ExceptionStackContext, self.exception_handler,
00155 self.active_cell),))
00156 return lambda: operator.setitem(self.active_cell, 0, False)
00157
00158 def __exit__(self, type, value, traceback):
00159 try:
00160 if type is not None:
00161 return self.exception_handler(type, value, traceback)
00162 finally:
00163 _state.contexts = self.old_contexts
00164
00165
00166 class NullContext(object):
00167 '''Resets the StackContext.
00168
00169 Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient)
00170 where the stack that caused the creating is not relevant to future
00171 operations.
00172 '''
00173 def __enter__(self):
00174 self.old_contexts = _state.contexts
00175 _state.contexts = ()
00176
00177 def __exit__(self, type, value, traceback):
00178 _state.contexts = self.old_contexts
00179
00180
00181 class _StackContextWrapper(functools.partial):
00182 pass
00183
00184
00185 def wrap(fn):
00186 '''Returns a callable object that will restore the current StackContext
00187 when executed.
00188
00189 Use this whenever saving a callback to be executed later in a
00190 different execution context (either in a different thread or
00191 asynchronously in the same thread).
00192 '''
00193 if fn is None or fn.__class__ is _StackContextWrapper:
00194 return fn
00195
00196
00197
00198 def wrapped(callback, contexts, *args, **kwargs):
00199 if contexts is _state.contexts or not contexts:
00200 callback(*args, **kwargs)
00201 return
00202 if not _state.contexts:
00203 new_contexts = [cls(arg, active_cell)
00204 for (cls, arg, active_cell) in contexts
00205 if active_cell[0]]
00206
00207
00208
00209
00210
00211
00212 elif (len(_state.contexts) > len(contexts) or
00213 any(a[1] is not b[1]
00214 for a, b in itertools.izip(_state.contexts, contexts))):
00215
00216 new_contexts = ([NullContext()] +
00217 [cls(arg, active_cell)
00218 for (cls, arg, active_cell) in contexts
00219 if active_cell[0]])
00220 else:
00221 new_contexts = [cls(arg, active_cell)
00222 for (cls, arg, active_cell) in contexts[len(_state.contexts):]
00223 if active_cell[0]]
00224 if len(new_contexts) > 1:
00225 with _nested(*new_contexts):
00226 callback(*args, **kwargs)
00227 elif new_contexts:
00228 with new_contexts[0]:
00229 callback(*args, **kwargs)
00230 else:
00231 callback(*args, **kwargs)
00232 if _state.contexts:
00233 return _StackContextWrapper(wrapped, fn, _state.contexts)
00234 else:
00235 return _StackContextWrapper(fn)
00236
00237
00238 @contextlib.contextmanager
00239 def _nested(*managers):
00240 """Support multiple context managers in a single with-statement.
00241
00242 Copied from the python 2.6 standard library. It's no longer present
00243 in python 3 because the with statement natively supports multiple
00244 context managers, but that doesn't help if the list of context
00245 managers is not known until runtime.
00246 """
00247 exits = []
00248 vars = []
00249 exc = (None, None, None)
00250 try:
00251 for mgr in managers:
00252 exit = mgr.__exit__
00253 enter = mgr.__enter__
00254 vars.append(enter())
00255 exits.append(exit)
00256 yield vars
00257 except:
00258 exc = sys.exc_info()
00259 finally:
00260 while exits:
00261 exit = exits.pop()
00262 try:
00263 if exit(*exc):
00264 exc = (None, None, None)
00265 except:
00266 exc = sys.exc_info()
00267 if exc != (None, None, None):
00268
00269
00270
00271 raise_exc_info(exc)