0001# (c) 2005 Ben Bangert
0002# This module is part of the Python Paste Project and is released under
0003# the MIT License: http://www.opensource.org/licenses/mit-license.php
0004"""Registry for handling request-local module globals sanely
0005
0006Dealing with module globals in a thread-safe way is good if your
0007application is the sole responder in a thread, however that approach fails
0008to properly account for various scenarios that occur with WSGI applications
0009and middleware.
0010
0011What is actually needed in the case where a module global is desired that
0012is always set properly depending on the current request, is a stacked
0013thread-local object. Such an object is popped or pushed during the request
0014cycle so that it properly represents the object that should be active for
0015the current request.
0016
0017To make it easy to deal with such variables, this module provides a special
0018StackedObjectProxy class which you can instantiate and attach to your
0019module where you'd like others to access it. The object you'd like this to
0020actually "be" during the request is then registered with the
0021RegistryManager middleware, which ensures that for the scope of the current
0022WSGI application everything will work properly.
0023
0024Example:
0025
0026.. code-block:: Python
0027
0028 #yourpackage/__init__.py
0029
0030 from paste.registry import RegistryManager, StackedObjectProxy
0031 myglobal = StackedObjectProxy()
0032
0033 #wsgi app stack
0034 app = RegistryManager(yourapp)
0035
0036 #inside your wsgi app
0037 class yourapp(object):
0038 def __call__(self, environ, start_response):
0039 obj = someobject # The request-local object you want to access
0040 # via yourpackage.myglobal
0041 if environ.has_key('paste.registry'):
0042 environ['paste.registry'].register(myglobal, obj)
0043
0044You will then be able to import yourpackage anywhere in your WSGI app or in
0045the calling stack below it and be assured that it is using the object you
0046registered with Registry.
0047
0048RegistryManager can be in the WSGI stack multiple times, each time it
0049appears it registers a new request context.
0050
0051
0052Performance
0053===========
0054
0055The overhead of the proxy object is very minimal, however if you are using
0056proxy objects extensively (Thousands of accesses per request or more), there
0057are some ways to avoid them. A proxy object runs approximately 3-20x slower
0058than direct access to the object, this is rarely your performance bottleneck
0059when developing web applications.
0060
0061Should you be developing a system which may be accessing the proxy object
0062thousands of times per request, the performance of the proxy will start to
0063become more noticeable. In that circumstance, the problem can be avoided by
0064getting at the actual object via the proxy with the ``_current_obj`` function:
0065
0066.. code-block:: Python
0067
0068 #sessions.py
0069 Session = StackedObjectProxy()
0070 # ... initialization code, etc.
0071
0072 # somemodule.py
0073 import sessions
0074
0075 def somefunc():
0076 session = sessions.Session._current_obj()
0077 # ... tons of session access
0078
0079This way the proxy is used only once to retrieve the object for the current
0080context and the overhead is minimized while still making it easy to access
0081the underlying object. The ``_current_obj`` function is preceded by an
0082underscore to more likely avoid clashing with the contained object's
0083attributes.
0084
0085**NOTE:** This is *highly* unlikely to be an issue in the vast majority of
0086cases, and requires incredibly large amounts of proxy object access before
0087one should consider the proxy object to be causing slow-downs. This section
0088is provided solely in the extremely rare case that it is an issue so that a
0089quick way to work around it is documented.
0090
0091"""
0092import sys
0093import paste.util.threadinglocal as threadinglocal
0094
0095__all__ = ['StackedObjectProxy', 'RegistryManager', 'StackedObjectRestorer',
0096 'restorer']
0097
0098class NoDefault(object): pass
0099
0100class StackedObjectProxy(object):
0101 """Track an object instance internally using a stack
0102
0103 The StackedObjectProxy proxies access to an object internally using a
0104 stacked thread-local. This makes it safe for complex WSGI environments
0105 where access to the object may be desired in multiple places without
0106 having to pass the actual object around.
0107
0108 New objects are added to the top of the stack with _push_object while
0109 objects can be removed with _pop_object.
0110
0111 """
0112 def __init__(self, default=NoDefault, name="Default"):
0113 """Create a new StackedObjectProxy
0114
0115 If a default is given, its used in every thread if no other object
0116 has been pushed on.
0117
0118 """
0119 self.__dict__['____name__'] = name
0120 self.__dict__['____local__'] = threadinglocal.local()
0121 if default is not NoDefault:
0122 self.__dict__['____default_object__'] = default
0123
0124 def __getattr__(self, attr):
0125 return getattr(self._current_obj(), attr)
0126
0127 def __setattr__(self, attr, value):
0128 setattr(self._current_obj(), attr, value)
0129
0130 def __delattr__(self, name):
0131 delattr(self._current_obj(), name)
0132
0133 def __getitem__(self, key):
0134 return self._current_obj()[key]
0135
0136 def __setitem__(self, key, value):
0137 self._current_obj()[key] = value
0138
0139 def __delitem__(self, key):
0140 del self._current_obj()[key]
0141
0142 def __call__(self, *args, **kw):
0143 return self._current_obj()(*args, **kw)
0144
0145 def __repr__(self):
0146 try:
0147 return repr(self._current_obj())
0148 except (TypeError, AttributeError):
0149 return '<%s.%s object at 0x%x>' % (self.__class__.__module__,
0150 self.__class__.__name__,
0151 id(self))
0152
0153 def __iter__(self):
0154 return iter(self._current_obj())
0155
0156 def __len__(self):
0157 return len(self._current_obj())
0158
0159 def __contains__(self, key):
0160 return key in self._current_obj()
0161
0162 def __nonzero__(self):
0163 return bool(self._current_obj())
0164
0165 def _current_obj(self):
0166 """Returns the current active object being proxied to
0167
0168 In the event that no object was pushed, the default object if
0169 provided will be used. Otherwise, a TypeError will be raised.
0170
0171 """
0172 objects = getattr(self.____local__, 'objects', None)
0173 if objects:
0174 return objects[-1]
0175 else:
0176 obj = self.__dict__.get('____default_object__', NoDefault)
0177 if obj is not NoDefault:
0178 return obj
0179 else:
0180 raise TypeError(
0181 'No object (name: %s) has been registered for this '
0182 'thread' % self.____name__)
0183
0184 def _push_object(self, obj):
0185 """Make ``obj`` the active object for this thread-local.
0186
0187 This should be used like:
0188
0189 .. code-block:: Python
0190
0191 obj = yourobject()
0192 module.glob = StackedObjectProxy()
0193 module.glob._push_object(obj)
0194 try:
0195 ... do stuff ...
0196 finally:
0197 module.glob._pop_object(conf)
0198
0199 """
0200 if not hasattr(self.____local__, 'objects'):
0201 self.____local__.objects = []
0202 self.____local__.objects.append(obj)
0203
0204 def _pop_object(self, obj=None):
0205 """Remove a thread-local object.
0206
0207 If ``obj`` is given, it is checked against the popped object and an
0208 error is emitted if they don't match.
0209
0210 """
0211 if not hasattr(self.____local__, 'objects'):
0212 raise AssertionError('No object has been registered for this thread')
0213 popped = self.____local__.objects.pop()
0214 if obj:
0215 if popped is not obj:
0216 raise AssertionError(
0217 'The object popped (%s) is not the same as the object '
0218 'expected (%s)' % (popped, obj))
0219
0220 def _object_stack(self):
0221 """Returns all of the objects stacked in this container
0222
0223 (Might return [] if there are none)
0224 """
0225 try:
0226 return self.____local__.objects[:]
0227 except AssertionError:
0228 return []
0229
0230 # The following methods will be swapped for their original versions by
0231 # StackedObjectRestorer when restoration is enabled. The original
0232 # functions (e.g. _current_obj) will be available at _current_obj_orig
0233
0234 def _current_obj_restoration(self):
0235 request_id = restorer.in_restoration()
0236 if request_id:
0237 return restorer.get_saved_proxied_obj(self, request_id)
0238 return self._current_obj_orig()
0239 _current_obj_restoration.__doc__ = ('%s\n(StackedObjectRestorer restoration enabled)' % _current_obj.__doc__)
0242
0243 def _push_object_restoration(self, obj):
0244 if not restorer.in_restoration():
0245 self._push_object_orig(obj)
0246 _push_object_restoration.__doc__ = ('%s\n(StackedObjectRestorer restoration enabled)' % _push_object.__doc__)
0249
0250 def _pop_object_restoration(self, obj=None):
0251 if not restorer.in_restoration():
0252 self._pop_object_orig(obj)
0253 _pop_object_restoration.__doc__ = ('%s\n(StackedObjectRestorer restoration enabled)' % _pop_object.__doc__)
0256
0257class Registry(object):
0258 """Track objects and stacked object proxies for removal
0259
0260 The Registry object is instantiated a single time for the request no
0261 matter how many times the RegistryManager is used in a WSGI stack. Each
0262 RegistryManager must call ``prepare`` before continuing the call to
0263 start a new context for object registering.
0264
0265 Each context is tracked with a dict inside a list. The last list
0266 element is the currently executing context. Each context dict is keyed
0267 by the id of the StackedObjectProxy instance being proxied, the value
0268 is a tuple of the StackedObjectProxy instance and the object being
0269 tracked.
0270
0271 """
0272 def __init__(self):
0273 """Create a new Registry object
0274
0275 ``prepare`` must still be called before this Registry object can be
0276 used to register objects.
0277
0278 """
0279 self.reglist = []
0280
0281 def prepare(self):
0282 """Used to create a new registry context
0283
0284 Anytime a new RegistryManager is called, ``prepare`` needs to be
0285 called on the existing Registry object. This sets up a new context
0286 for registering objects.
0287
0288 """
0289 self.reglist.append({})
0290
0291 def register(self, stacked, obj):
0292 """Register an object with a StackedObjectProxy"""
0293 stacked._push_object(obj)
0294 myreglist = self.reglist[-1]
0295 myreglist[id(stacked)] = (stacked, obj)
0296
0297 def replace(self, stacked, obj):
0298 """Replace the object referenced by a StackedObjectProxy with a
0299 different object
0300
0301 In the event that no object has been registered, the new object will
0302 be registered.
0303 """
0304 myreglist = self.reglist[-1]
0305 if id(stacked) in myreglist:
0306 stacked._pop_object(myreglist[id(stacked)][1])
0307 self.register(stacked, obj)
0308
0309 def cleanup(self):
0310 """Remove all objects from all StackedObjectProxy instances that
0311 were tracked at this Registry context"""
0312 for id, val in self.reglist[-1].iteritems():
0313 stacked, obj = val
0314 stacked._pop_object(obj)
0315 self.reglist.pop()
0316
0317class RegistryManager(object):
0318 """Creates and maintains a Registry context
0319
0320 RegistryManager creates a new registry context for the registration of
0321 StackedObjectProxy instances. Multiple RegistryManager's can be in a
0322 WSGI stack and will manage the context so that the StackedObjectProxies
0323 always proxy to the proper object.
0324
0325 The object being registered can be any object sub-class, list, or dict.
0326
0327 Registering objects is done inside a WSGI application under the
0328 RegistryManager instance, using the ``environ['paste.registry']``
0329 object which is a Registry instance.
0330
0331 """
0332 def __init__(self, application):
0333 self.application = application
0334
0335 def __call__(self, environ, start_response):
0336 app_iter = None
0337 reg = environ.setdefault('paste.registry', Registry())
0338 reg.prepare()
0339 try:
0340 app_iter = self.application(environ, start_response)
0341 except Exception, e:
0342 # Regardless of if the content is an iterable, generator, list
0343 # or tuple, we clean-up right now. If its an iterable/generator
0344 # care should be used to ensure the generator has its own ref
0345 # to the actual object
0346 if environ.get('paste.evalexception'):
0347 # EvalException is present in the WSGI stack
0348 expected = False
0349 for expect in environ.get('paste.expected_exceptions', []):
0350 if isinstance(e, expect):
0351 expected = True
0352 if not expected:
0353 # An unexpected exception: save state for EvalException
0354 restorer.save_registry_state(environ)
0355 reg.cleanup()
0356 raise
0357 except:
0358 # Save state for EvalException if it's present
0359 if environ.get('paste.evalexception'):
0360 restorer.save_registry_state(environ)
0361 reg.cleanup()
0362 raise
0363 else:
0364 reg.cleanup()
0365
0366 return app_iter
0367
0368class StackedObjectRestorer(object):
0369 """Track StackedObjectProxies and their proxied objects for automatic
0370 restoration within EvalException's interactive debugger.
0371
0372 An instance of this class tracks all StackedObjectProxy state in existence
0373 when unexpected exceptions are raised by WSGI applications housed by
0374 EvalException and RegistryManager. Like EvalException, this information is
0375 stored for the life of the process.
0376
0377 When an unexpected exception occurs and EvalException is present in the
0378 WSGI stack, save_registry_state is intended to be called to store the
0379 Registry state and enable automatic restoration on all currently registered
0380 StackedObjectProxies.
0381
0382 With restoration enabled, those StackedObjectProxies' _current_obj
0383 (overwritten by _current_obj_restoration) method's strategy is modified:
0384 it will return its appropriate proxied object from the restorer when
0385 a restoration context is active in the current thread.
0386
0387 The StackedObjectProxies' _push/pop_object methods strategies are also
0388 changed: they no-op when a restoration context is active in the current
0389 thread (because the pushing/popping work is all handled by the
0390 Registry/restorer).
0391
0392 The request's Registry objects' reglists are restored from the restorer
0393 when a restoration context begins, enabling the Registry methods to work
0394 while their changes are tracked by the restorer.
0395
0396 The overhead of enabling restoration is negligible (another threadlocal
0397 access for the changed StackedObjectProxy methods) for normal use outside
0398 of a restoration context, but worth mentioning when combined with
0399 StackedObjectProxies normal overhead. Once enabled it does not turn off,
0400 however:
0401
0402 o Enabling restoration only occurs after an unexpected exception is
0403 detected. The server is likely to be restarted shortly after the exception
0404 is raised to fix the cause
0405
0406 o StackedObjectRestorer is only enabled when EvalException is enabled (not
0407 on a production server) and RegistryManager exists in the middleware
0408 stack"""
0409 def __init__(self):
0410 # Registries and their saved reglists by request_id
0411 self.saved_registry_states = {}
0412 self.restoration_context_id = threadinglocal.local()
0413
0414 def save_registry_state(self, environ):
0415 """Save the state of this request's Registry (if it hasn't already been
0416 saved) to the saved_registry_states dict, keyed by the request's unique
0417 identifier"""
0418 registry = environ.get('paste.registry')
0419 if not registry or not len(registry.reglist) or self.get_request_id(environ) in self.saved_registry_states:
0421 # No Registry, no state to save, or this request's state has
0422 # already been saved
0423 return
0424
0425 self.saved_registry_states[self.get_request_id(environ)] = (registry, registry.reglist[:])
0427
0428 # Tweak the StackedObjectProxies we want to save state for -- change
0429 # their methods to act differently when a restoration context is active
0430 # in the current thread
0431 for reglist in registry.reglist:
0432 for stacked, obj in reglist.itervalues():
0433 self.enable_restoration(stacked)
0434
0435 def get_saved_proxied_obj(self, stacked, request_id):
0436 """Retrieve the saved object proxied by the specified
0437 StackedObjectProxy for the request identified by request_id"""
0438 # All state for the request identified by request_id
0439 reglist = self.saved_registry_states[request_id][1]
0440
0441 # The top of the stack was current when the exception occurred
0442 stack_level = len(reglist) - 1
0443 stacked_id = id(stacked)
0444 while True:
0445 if stack_level < 0:
0446 # Nothing registered: Call _current_obj_orig to raise a
0447 # TypeError
0448 return stacked._current_obj_orig()
0449 context = reglist[stack_level]
0450 if stacked_id in context:
0451 break
0452 # This StackedObjectProxy may not have been registered by the
0453 # RegistryManager that was active when the exception was raised --
0454 # continue searching down the stack until it's found
0455 stack_level -= 1
0456 return context[stacked_id][1]
0457
0458 def enable_restoration(self, stacked):
0459 """Replace the specified StackedObjectProxy's methods with their
0460 respective restoration versions.
0461
0462 _current_obj_restoration forces recovery of the saved proxied object
0463 when a restoration context is active in the current thread.
0464
0465 _push/pop_object_restoration avoid pushing/popping data
0466 (pushing/popping is only done at the Registry level) when a restoration
0467 context is active in the current thread"""
0468 if '_current_obj_orig' in stacked.__dict__:
0469 # Restoration already enabled
0470 return
0471
0472 for func_name in ('_current_obj', '_push_object', '_pop_object'):
0473 orig_func = getattr(stacked, func_name)
0474 restoration_func = getattr(stacked, func_name + '_restoration')
0475 stacked.__dict__[func_name + '_orig'] = orig_func
0476 stacked.__dict__[func_name] = restoration_func
0477
0478 def get_request_id(self, environ):
0479 """Return a unique identifier for the current request"""
0480 from paste.evalexception.middleware import get_debug_count
0481 return get_debug_count(environ)
0482
0483 def restoration_begin(self, request_id):
0484 """Enable a restoration context in the current thread for the specified
0485 request_id"""
0486 if request_id in self.saved_registry_states:
0487 # Restore the old Registry object's state
0488 registry, reglist = self.saved_registry_states[request_id]
0489 registry.reglist = reglist
0490
0491 self.restoration_context_id.request_id = request_id
0492
0493 def restoration_end(self):
0494 """Register a restoration context as finished, if one exists"""
0495 try:
0496 del self.restoration_context_id.request_id
0497 except AttributeError:
0498 pass
0499
0500 def in_restoration(self):
0501 """Determine if a restoration context is active for the current thread.
0502 Returns the request_id it's active for if so, otherwise False"""
0503 return getattr(self.restoration_context_id, 'request_id', False)
0504
0505restorer = StackedObjectRestorer()
0506
0507
0508# Paste Deploy entry point
0509def make_registry_manager(app, global_conf):
0510 return RegistryManager(app)
0511
0512make_registry_manager.__doc__ = RegistryManager.__doc__