388 lines
13 KiB
Python
388 lines
13 KiB
Python
#!/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 state
|
|
that 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 to
|
|
allow some additional context to be kept for logging.
|
|
|
|
This is slightly magic, but it's an extension of the idea that an
|
|
exception handler is a kind of stack-local state and when that stack
|
|
is suspended and resumed in a new context that state needs to be
|
|
preserved. `StackContext` shifts the burden of restoring that state
|
|
from each call site (e.g. wrapping each `.AsyncHTTPClient` callback
|
|
in ``async_callback``) to the mechanisms that transfer control from
|
|
one 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__ import absolute_import, division, print_function, with_statement
|
|
|
|
import sys
|
|
import threading
|
|
|
|
from .util import raise_exc_info
|
|
|
|
|
|
class StackContextInconsistentError(Exception):
|
|
pass
|
|
|
|
|
|
class _State(threading.local):
|
|
def __init__(self):
|
|
self.contexts = (tuple(), None)
|
|
_state = _State()
|
|
|
|
|
|
class StackContext(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_factory
|
|
self.contexts = []
|
|
self.active = True
|
|
|
|
def _deactivate(self):
|
|
self.active = False
|
|
|
|
# StackContext protocol
|
|
def enter(self):
|
|
context = self.context_factory()
|
|
self.contexts.append(context)
|
|
context.__enter__()
|
|
|
|
def exit(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.contexts
|
|
self.new_contexts = (self.old_contexts[0] + (self,), self)
|
|
_state.contexts = self.new_contexts
|
|
|
|
try:
|
|
self.enter()
|
|
except:
|
|
_state.contexts = self.old_contexts
|
|
raise
|
|
|
|
return self._deactivate
|
|
|
|
def __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.
|
|
if final_contexts is not self.new_contexts:
|
|
raise StackContextInconsistentError(
|
|
'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
|
|
|
|
|
|
class ExceptionStackContext(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_handler
|
|
self.active = True
|
|
|
|
def _deactivate(self):
|
|
self.active = False
|
|
|
|
def exit(self, type, value, traceback):
|
|
if type is not None:
|
|
return self.exception_handler(type, value, traceback)
|
|
|
|
def __enter__(self):
|
|
self.old_contexts = _state.contexts
|
|
self.new_contexts = (self.old_contexts[0], self)
|
|
_state.contexts = self.new_contexts
|
|
|
|
return self._deactivate
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
try:
|
|
if type is not None:
|
|
return self.exception_handler(type, value, traceback)
|
|
finally:
|
|
final_contexts = _state.contexts
|
|
_state.contexts = self.old_contexts
|
|
|
|
if final_contexts is not self.new_contexts:
|
|
raise StackContextInconsistentError(
|
|
'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
|
|
|
|
|
|
class NullContext(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
|
|
|
|
|
|
def _remove_deactivated(contexts):
|
|
"""Remove deactivated handlers from the chain"""
|
|
# Clean ctx handlers
|
|
stack_contexts = tuple([h for h in contexts[0] if h.active])
|
|
|
|
# Find new head
|
|
head = contexts[1]
|
|
while head is not None and not head.active:
|
|
head = head.old_contexts[1]
|
|
|
|
# Process chain
|
|
ctx = head
|
|
while ctx is not None:
|
|
parent = ctx.old_contexts[1]
|
|
|
|
while parent is not None:
|
|
if parent.active:
|
|
break
|
|
ctx.old_contexts = parent.old_contexts
|
|
parent = parent.old_contexts[1]
|
|
|
|
ctx = parent
|
|
|
|
return (stack_contexts, head)
|
|
|
|
|
|
def wrap(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 wrapped
|
|
if fn is None or hasattr(fn, '_wrapped'):
|
|
return fn
|
|
|
|
# Capture current stack head
|
|
# TODO: Any other better way to store contexts and update them in wrapped function?
|
|
cap_contexts = [_state.contexts]
|
|
|
|
if not cap_contexts[0][0] and not cap_contexts[0][1]:
|
|
# Fast path when there are no active contexts.
|
|
def null_wrapper(*args, **kwargs):
|
|
try:
|
|
current_state = _state.contexts
|
|
_state.contexts = cap_contexts[0]
|
|
return fn(*args, **kwargs)
|
|
finally:
|
|
_state.contexts = current_state
|
|
null_wrapper._wrapped = True
|
|
return null_wrapper
|
|
|
|
def wrapped(*args, **kwargs):
|
|
ret = None
|
|
try:
|
|
# Capture old state
|
|
current_state = _state.contexts
|
|
|
|
# Remove deactivated items
|
|
cap_contexts[0] = contexts = _remove_deactivated(cap_contexts[0])
|
|
|
|
# Force new state
|
|
_state.contexts = contexts
|
|
|
|
# Current exception
|
|
exc = (None, None, None)
|
|
top = None
|
|
|
|
# Apply stack contexts
|
|
last_ctx = 0
|
|
stack = contexts[0]
|
|
|
|
# Apply state
|
|
for n in stack:
|
|
try:
|
|
n.enter()
|
|
last_ctx += 1
|
|
except:
|
|
# Exception happened. Record exception info and store top-most handler
|
|
exc = sys.exc_info()
|
|
top = n.old_contexts[1]
|
|
|
|
# Execute callback if no exception happened while restoring state
|
|
if top is None:
|
|
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 chain
|
|
if top is not None:
|
|
exc = _handle_exception(top, exc)
|
|
else:
|
|
# Otherwise take shorter path and run stack contexts in reverse order
|
|
while last_ctx > 0:
|
|
last_ctx -= 1
|
|
c = stack[last_ctx]
|
|
|
|
try:
|
|
c.exit(*exc)
|
|
except:
|
|
exc = sys.exc_info()
|
|
top = c.old_contexts[1]
|
|
break
|
|
else:
|
|
top = None
|
|
|
|
# If if exception happened while unrolling, take longer exception handler path
|
|
if top is not None:
|
|
exc = _handle_exception(top, exc)
|
|
|
|
# If exception was not handled, raise it
|
|
if exc != (None, None, None):
|
|
raise_exc_info(exc)
|
|
finally:
|
|
_state.contexts = current_state
|
|
return ret
|
|
|
|
wrapped._wrapped = True
|
|
return wrapped
|
|
|
|
|
|
def _handle_exception(tail, exc):
|
|
while tail is not None:
|
|
try:
|
|
if tail.exit(*exc):
|
|
exc = (None, None, None)
|
|
except:
|
|
exc = sys.exc_info()
|
|
|
|
tail = tail.old_contexts[1]
|
|
|
|
return exc
|
|
|
|
|
|
def run_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
|
|
"""
|
|
with context:
|
|
return func()
|