Source code for tornado.stack_context

#!/usr/bin/env python## Copyright 2010 Facebook## Licensed under the Apache License, Version 2.0 (the "License"); you may# not use this file except in compliance with the License. You may obtain# a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the# License for the specific language governing permissions and limitations# under the License."""`StackContext` allows applications to maintain threadlocal-like statethat follows execution as it moves to other execution contexts.The motivating examples are to eliminate the need for explicit``async_callback`` wrappers (as in `tornado.web.RequestHandler`), and toallow some additional context to be kept for logging.This is slightly magic, but it's an extension of the idea that anexception handler is a kind of stack-local state and when that stackis suspended and resumed in a new context that state needs to bepreserved. `StackContext` shifts the burden of restoring that statefrom each call site (e.g. wrapping each `.AsyncHTTPClient` callbackin ``async_callback``) to the mechanisms that transfer control fromone context to another (e.g. `.AsyncHTTPClient` itself, `.IOLoop`,thread pools, etc).Example usage:: @contextlib.contextmanager def die_on_error(): try: yield except Exception: logging.error("exception in asynchronous operation",exc_info=True) sys.exit(1) with StackContext(die_on_error): # Any exception thrown here *or in callback and its descendants* # will cause the process to exit instead of spinning endlessly # in the ioloop. http_client.fetch(url, callback) ioloop.start()Most applications shouldn't have to work with `StackContext` directly.Here are a few rules of thumb for when it's necessary:* If you're writing an asynchronous library that doesn't rely on a stack_context-aware library like `tornado.ioloop` or `tornado.iostream` (for example, if you're writing a thread pool), use `.stack_context.wrap()` before any asynchronous operations to capture the stack context from where the operation was started.* If you're writing an asynchronous library that has some shared resources (such as a connection pool), create those shared resources within a ``with stack_context.NullContext():`` block. This will prevent ``StackContexts`` from leaking from one request to another.* If you want to write something like an exception handler that will persist across asynchronous calls, create a new `StackContext` (or `ExceptionStackContext`), and make your asynchronous calls in a ``with`` block that references your `StackContext`."""from__future__importabsolute_import,division,print_function,with_statementimportsysimportthreadingfromtornado.utilimportraise_exc_infoclassStackContextInconsistentError(Exception):passclass_State(threading.local):def__init__(self):self.contexts=(tuple(),None)_state=_State()

[docs]classStackContext(object):"""Establishes the given context as a StackContext that will be transferred. Note that the parameter is a callable that returns a context manager, not the context itself. That is, where for a non-transferable context manager you would say:: with my_context(): StackContext takes the function itself rather than its result:: with StackContext(my_context): The result of ``with StackContext() as cb:`` is a deactivation callback. Run this callback when the StackContext is no longer needed to ensure that it is not propagated any further (note that deactivating a context does not affect any instances of that context that are currently pending). This is an advanced feature and not necessary in most applications. """def__init__(self,context_factory):self.context_factory=context_factoryself.contexts=[]self.active=Truedef_deactivate(self):self.active=False# StackContext protocoldefenter(self):context=self.context_factory()self.contexts.append(context)context.__enter__()defexit(self,type,value,traceback):context=self.contexts.pop()context.__exit__(type,value,traceback)# Note that some of this code is duplicated in ExceptionStackContext# below. ExceptionStackContext is more common and doesn't need# the full generality of this class.def__enter__(self):self.old_contexts=_state.contextsself.new_contexts=(self.old_contexts[0]+(self,),self)_state.contexts=self.new_contextstry:self.enter()except:_state.contexts=self.old_contextsraisereturnself._deactivatedef__exit__(self,type,value,traceback):try:self.exit(type,value,traceback)finally:final_contexts=_state.contexts_state.contexts=self.old_contexts# Generator coroutines and with-statements with non-local# effects interact badly. Check here for signs of# the stack getting out of sync.# Note that this check comes after restoring _state.context# so that if it fails things are left in a (relatively)# consistent state.iffinal_contextsisnotself.new_contexts:raiseStackContextInconsistentError('stack_context inconsistency (may be caused by yield ''within a "with StackContext" block)')# Break up a reference to itself to allow for faster GC on CPython.self.new_contexts=None

[docs]classExceptionStackContext(object):"""Specialization of StackContext for exception handling. The supplied ``exception_handler`` function will be called in the event of an uncaught exception in this context. The semantics are similar to a try/finally clause, and intended use cases are to log an error, close a socket, or similar cleanup actions. The ``exc_info`` triple ``(type, value, traceback)`` will be passed to the exception_handler function. If the exception handler returns true, the exception will be consumed and will not be propagated to other exception handlers. """def__init__(self,exception_handler):self.exception_handler=exception_handlerself.active=Truedef_deactivate(self):self.active=Falsedefexit(self,type,value,traceback):iftypeisnotNone:returnself.exception_handler(type,value,traceback)def__enter__(self):self.old_contexts=_state.contextsself.new_contexts=(self.old_contexts[0],self)_state.contexts=self.new_contextsreturnself._deactivatedef__exit__(self,type,value,traceback):try:iftypeisnotNone:returnself.exception_handler(type,value,traceback)finally:final_contexts=_state.contexts_state.contexts=self.old_contextsiffinal_contextsisnotself.new_contexts:raiseStackContextInconsistentError('stack_context inconsistency (may be caused by yield ''within a "with StackContext" block)')# Break up a reference to itself to allow for faster GC on CPython.self.new_contexts=None

[docs]classNullContext(object):"""Resets the `StackContext`. Useful when creating a shared resource on demand (e.g. an `.AsyncHTTPClient`) where the stack that caused the creating is not relevant to future operations. """def__enter__(self):self.old_contexts=_state.contexts_state.contexts=(tuple(),None)def__exit__(self,type,value,traceback):_state.contexts=self.old_contexts

[docs]defwrap(fn):"""Returns a callable object that will restore the current `StackContext` when executed. Use this whenever saving a callback to be executed later in a different execution context (either in a different thread or asynchronously in the same thread). """# Check if function is already wrappediffnisNoneorhasattr(fn,'_wrapped'):returnfn# Capture current stack head# TODO: Any other better way to store contexts and update them in wrapped function?cap_contexts=[_state.contexts]ifnotcap_contexts[0][0]andnotcap_contexts[0][1]:# Fast path when there are no active contexts.defnull_wrapper(*args,**kwargs):try:current_state=_state.contexts_state.contexts=cap_contexts[0]returnfn(*args,**kwargs)finally:_state.contexts=current_statenull_wrapper._wrapped=Truereturnnull_wrapperdefwrapped(*args,**kwargs):ret=Nonetry:# Capture old statecurrent_state=_state.contexts# Remove deactivated itemscap_contexts[0]=contexts=_remove_deactivated(cap_contexts[0])# Force new state_state.contexts=contexts# Current exceptionexc=(None,None,None)top=None# Apply stack contextslast_ctx=0stack=contexts[0]# Apply stateforninstack:try:n.enter()last_ctx+=1except:# Exception happened. Record exception info and store top-most handlerexc=sys.exc_info()top=n.old_contexts[1]# Execute callback if no exception happened while restoring stateiftopisNone:try:ret=fn(*args,**kwargs)except:exc=sys.exc_info()top=contexts[1]# If there was exception, try to handle it by going through the exception chainiftopisnotNone:exc=_handle_exception(top,exc)else:# Otherwise take shorter path and run stack contexts in reverse orderwhilelast_ctx>0:last_ctx-=1c=stack[last_ctx]try:c.exit(*exc)except:exc=sys.exc_info()top=c.old_contexts[1]breakelse:top=None# If if exception happened while unrolling, take longer exception handler pathiftopisnotNone:exc=_handle_exception(top,exc)# If exception was not handled, raise itifexc!=(None,None,None):raise_exc_info(exc)finally:_state.contexts=current_statereturnretwrapped._wrapped=Truereturnwrapped

[docs]defrun_with_stack_context(context,func):"""Run a coroutine ``func`` in the given `StackContext`. It is not safe to have a ``yield`` statement within a ``with StackContext`` block, so it is difficult to use stack context with `.gen.coroutine`. This helper function runs the function in the correct context while keeping the ``yield`` and ``with`` statements syntactically separate. Example:: @gen.coroutine def incorrect(): with StackContext(ctx): # ERROR: this will raise StackContextInconsistentError yield other_coroutine() @gen.coroutine def correct(): yield run_with_stack_context(StackContext(ctx), other_coroutine) .. versionadded:: 3.1 """withcontext:returnfunc()