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
00025 exception handler is a kind of stack-local state and when that stack
00026 is suspended and resumed in a new context that state needs to be
00027 preserved. `StackContext` shifts the burden of restoring that state
00028 from each call site (e.g. wrapping each `.AsyncHTTPClient` callback
00029 in ``async_callback``) to the mechanisms that transfer control from
00030 one context to another (e.g. `.AsyncHTTPClient` itself, `.IOLoop`,
00031 thread pools, etc).
00032
00033 Example usage::
00034
00035 @contextlib.contextmanager
00036 def die_on_error():
00037 try:
00038 yield
00039 except Exception:
00040 logging.error("exception in asynchronous operation",exc_info=True)
00041 sys.exit(1)
00042
00043 with StackContext(die_on_error):
00044 # Any exception thrown here *or in callback and its desendents*
00045 # will cause the process to exit instead of spinning endlessly
00046 # in the ioloop.
00047 http_client.fetch(url, callback)
00048 ioloop.start()
00049
00050 Most applications shouln't have to work with `StackContext` directly.
00051 Here are a few rules of thumb for when it's necessary:
00052
00053 * If you're writing an asynchronous library that doesn't rely on a
00054 stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
00055 (for example, if you're writing a thread pool), use
00056 `.stack_context.wrap()` before any asynchronous operations to capture the
00057 stack context from where the operation was started.
00058
00059 * If you're writing an asynchronous library that has some shared
00060 resources (such as a connection pool), create those shared resources
00061 within a ``with stack_context.NullContext():`` block. This will prevent
00062 ``StackContexts`` from leaking from one request to another.
00063
00064 * If you want to write something like an exception handler that will
00065 persist across asynchronous calls, create a new `StackContext` (or
00066 `ExceptionStackContext`), and make your asynchronous calls in a ``with``
00067 block that references your `StackContext`.
00068 """
00069
00070 from __future__ import absolute_import, division, print_function, with_statement
00071
00072 import sys
00073 import threading
00074
00075 from tornado.util import raise_exc_info
00076
00077
00078 class StackContextInconsistentError(Exception):
00079 pass
00080
00081
00082 class _State(threading.local):
00083 def __init__(self):
00084 self.contexts = (tuple(), None)
00085 _state = _State()
00086
00087
00088 class StackContext(object):
00089 """Establishes the given context as a StackContext that will be transferred.
00090
00091 Note that the parameter is a callable that returns a context
00092 manager, not the context itself. That is, where for a
00093 non-transferable context manager you would say::
00094
00095 with my_context():
00096
00097 StackContext takes the function itself rather than its result::
00098
00099 with StackContext(my_context):
00100
00101 The result of ``with StackContext() as cb:`` is a deactivation
00102 callback. Run this callback when the StackContext is no longer
00103 needed to ensure that it is not propagated any further (note that
00104 deactivating a context does not affect any instances of that
00105 context that are currently pending). This is an advanced feature
00106 and not necessary in most applications.
00107 """
00108 def __init__(self, context_factory):
00109 self.context_factory = context_factory
00110 self.contexts = []
00111 self.active = True
00112
00113 def _deactivate(self):
00114 self.active = False
00115
00116
00117 def enter(self):
00118 context = self.context_factory()
00119 self.contexts.append(context)
00120 context.__enter__()
00121
00122 def exit(self, type, value, traceback):
00123 context = self.contexts.pop()
00124 context.__exit__(type, value, traceback)
00125
00126
00127
00128
00129 def __enter__(self):
00130 self.old_contexts = _state.contexts
00131 self.new_contexts = (self.old_contexts[0] + (self,), self)
00132 _state.contexts = self.new_contexts
00133
00134 try:
00135 self.enter()
00136 except:
00137 _state.contexts = self.old_contexts
00138 raise
00139
00140 return self._deactivate
00141
00142 def __exit__(self, type, value, traceback):
00143 try:
00144 self.exit(type, value, traceback)
00145 finally:
00146 final_contexts = _state.contexts
00147 _state.contexts = self.old_contexts
00148
00149
00150
00151
00152
00153
00154
00155 if final_contexts is not self.new_contexts:
00156 raise StackContextInconsistentError(
00157 'stack_context inconsistency (may be caused by yield '
00158 'within a "with StackContext" block)')
00159
00160
00161 self.new_contexts = None
00162
00163
00164 class ExceptionStackContext(object):
00165 """Specialization of StackContext for exception handling.
00166
00167 The supplied ``exception_handler`` function will be called in the
00168 event of an uncaught exception in this context. The semantics are
00169 similar to a try/finally clause, and intended use cases are to log
00170 an error, close a socket, or similar cleanup actions. The
00171 ``exc_info`` triple ``(type, value, traceback)`` will be passed to the
00172 exception_handler function.
00173
00174 If the exception handler returns true, the exception will be
00175 consumed and will not be propagated to other exception handlers.
00176 """
00177 def __init__(self, exception_handler):
00178 self.exception_handler = exception_handler
00179 self.active = True
00180
00181 def _deactivate(self):
00182 self.active = False
00183
00184 def exit(self, type, value, traceback):
00185 if type is not None:
00186 return self.exception_handler(type, value, traceback)
00187
00188 def __enter__(self):
00189 self.old_contexts = _state.contexts
00190 self.new_contexts = (self.old_contexts[0], self)
00191 _state.contexts = self.new_contexts
00192
00193 return self._deactivate
00194
00195 def __exit__(self, type, value, traceback):
00196 try:
00197 if type is not None:
00198 return self.exception_handler(type, value, traceback)
00199 finally:
00200 final_contexts = _state.contexts
00201 _state.contexts = self.old_contexts
00202
00203 if final_contexts is not self.new_contexts:
00204 raise StackContextInconsistentError(
00205 'stack_context inconsistency (may be caused by yield '
00206 'within a "with StackContext" block)')
00207
00208
00209 self.new_contexts = None
00210
00211
00212 class NullContext(object):
00213 """Resets the `StackContext`.
00214
00215 Useful when creating a shared resource on demand (e.g. an
00216 `.AsyncHTTPClient`) where the stack that caused the creating is
00217 not relevant to future operations.
00218 """
00219 def __enter__(self):
00220 self.old_contexts = _state.contexts
00221 _state.contexts = (tuple(), None)
00222
00223 def __exit__(self, type, value, traceback):
00224 _state.contexts = self.old_contexts
00225
00226
00227 def _remove_deactivated(contexts):
00228 """Remove deactivated handlers from the chain"""
00229
00230 stack_contexts = tuple([h for h in contexts[0] if h.active])
00231
00232
00233 head = contexts[1]
00234 while head is not None and not head.active:
00235 head = head.old_contexts[1]
00236
00237
00238 ctx = head
00239 while ctx is not None:
00240 parent = ctx.old_contexts[1]
00241
00242 while parent is not None:
00243 if parent.active:
00244 break
00245 ctx.old_contexts = parent.old_contexts
00246 parent = parent.old_contexts[1]
00247
00248 ctx = parent
00249
00250 return (stack_contexts, head)
00251
00252
00253 def wrap(fn):
00254 """Returns a callable object that will restore the current `StackContext`
00255 when executed.
00256
00257 Use this whenever saving a callback to be executed later in a
00258 different execution context (either in a different thread or
00259 asynchronously in the same thread).
00260 """
00261
00262 if fn is None or hasattr(fn, '_wrapped'):
00263 return fn
00264
00265
00266
00267 cap_contexts = [_state.contexts]
00268
00269 if not cap_contexts[0][0] and not cap_contexts[0][1]:
00270
00271 def null_wrapper(*args, **kwargs):
00272 try:
00273 current_state = _state.contexts
00274 _state.contexts = cap_contexts[0]
00275 return fn(*args, **kwargs)
00276 finally:
00277 _state.contexts = current_state
00278 null_wrapper._wrapped = True
00279 return null_wrapper
00280
00281 def wrapped(*args, **kwargs):
00282 ret = None
00283 try:
00284
00285 current_state = _state.contexts
00286
00287
00288 cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
00289
00290
00291 _state.contexts = contexts
00292
00293
00294 exc = (None, None, None)
00295 top = None
00296
00297
00298 last_ctx = 0
00299 stack = contexts[0]
00300
00301
00302 for n in stack:
00303 try:
00304 n.enter()
00305 last_ctx += 1
00306 except:
00307
00308 exc = sys.exc_info()
00309 top = n.old_contexts[1]
00310
00311
00312 if top is None:
00313 try:
00314 ret = fn(*args, **kwargs)
00315 except:
00316 exc = sys.exc_info()
00317 top = contexts[1]
00318
00319
00320 if top is not None:
00321 exc = _handle_exception(top, exc)
00322 else:
00323
00324 while last_ctx > 0:
00325 last_ctx -= 1
00326 c = stack[last_ctx]
00327
00328 try:
00329 c.exit(*exc)
00330 except:
00331 exc = sys.exc_info()
00332 top = c.old_contexts[1]
00333 break
00334 else:
00335 top = None
00336
00337
00338 if top is not None:
00339 exc = _handle_exception(top, exc)
00340
00341
00342 if exc != (None, None, None):
00343 raise_exc_info(exc)
00344 finally:
00345 _state.contexts = current_state
00346 return ret
00347
00348 wrapped._wrapped = True
00349 return wrapped
00350
00351
00352 def _handle_exception(tail, exc):
00353 while tail is not None:
00354 try:
00355 if tail.exit(*exc):
00356 exc = (None, None, None)
00357 except:
00358 exc = sys.exc_info()
00359
00360 tail = tail.old_contexts[1]
00361
00362 return exc
00363
00364
00365 def run_with_stack_context(context, func):
00366 """Run a coroutine ``func`` in the given `StackContext`.
00367
00368 It is not safe to have a ``yield`` statement within a ``with StackContext``
00369 block, so it is difficult to use stack context with `.gen.coroutine`.
00370 This helper function runs the function in the correct context while
00371 keeping the ``yield`` and ``with`` statements syntactically separate.
00372
00373 Example::
00374
00375 @gen.coroutine
00376 def incorrect():
00377 with StackContext(ctx):
00378 # ERROR: this will raise StackContextInconsistentError
00379 yield other_coroutine()
00380
00381 @gen.coroutine
00382 def correct():
00383 yield run_with_stack_context(StackContext(ctx), other_coroutine)
00384
00385 .. versionadded:: 3.1
00386 """
00387 with context:
00388 return func()