stack_context.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2010 Facebook
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License"); you may
00006 # not use this file except in compliance with the License. You may obtain
00007 # a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
00013 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
00014 # License for the specific language governing permissions and limitations
00015 # under the License.
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     # StackContext protocol
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     # Note that some of this code is duplicated in ExceptionStackContext
00127     # below.  ExceptionStackContext is more common and doesn't need
00128     # the full generality of this class.
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             # Generator coroutines and with-statements with non-local
00150             # effects interact badly.  Check here for signs of
00151             # the stack getting out of sync.
00152             # Note that this check comes after restoring _state.context
00153             # so that if it fails things are left in a (relatively)
00154             # consistent state.
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             # Break up a reference to itself to allow for faster GC on CPython.
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             # Break up a reference to itself to allow for faster GC on CPython.
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     # Clean ctx handlers
00230     stack_contexts = tuple([h for h in contexts[0] if h.active])
00231 
00232     # Find new head
00233     head = contexts[1]
00234     while head is not None and not head.active:
00235         head = head.old_contexts[1]
00236 
00237     # Process chain
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     # Check if function is already wrapped
00262     if fn is None or hasattr(fn, '_wrapped'):
00263         return fn
00264 
00265     # Capture current stack head
00266     # TODO: Any other better way to store contexts and update them in wrapped function?
00267     cap_contexts = [_state.contexts]
00268 
00269     if not cap_contexts[0][0] and not cap_contexts[0][1]:
00270         # Fast path when there are no active contexts.
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             # Capture old state
00285             current_state = _state.contexts
00286 
00287             # Remove deactivated items
00288             cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
00289 
00290             # Force new state
00291             _state.contexts = contexts
00292 
00293             # Current exception
00294             exc = (None, None, None)
00295             top = None
00296 
00297             # Apply stack contexts
00298             last_ctx = 0
00299             stack = contexts[0]
00300 
00301             # Apply state
00302             for n in stack:
00303                 try:
00304                     n.enter()
00305                     last_ctx += 1
00306                 except:
00307                     # Exception happened. Record exception info and store top-most handler
00308                     exc = sys.exc_info()
00309                     top = n.old_contexts[1]
00310 
00311             # Execute callback if no exception happened while restoring state
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             # If there was exception, try to handle it by going through the exception chain
00320             if top is not None:
00321                 exc = _handle_exception(top, exc)
00322             else:
00323                 # Otherwise take shorter path and run stack contexts in reverse order
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                 # If if exception happened while unrolling, take longer exception handler path
00338                 if top is not None:
00339                     exc = _handle_exception(top, exc)
00340 
00341             # If exception was not handled, raise it
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()


rosbridge_tools
Author(s): Jonathan Mace
autogenerated on Sat Dec 27 2014 11:25:59