1# (c) 2005 Ben Bangert 2# This module is part of the Python Paste Project and is released under 3# the MIT License: http://www.opensource.org/licenses/mit-license.php 4"""Registry for handling request-local module globals sanely 5 6Dealing with module globals in a thread-safe way is good if your 7application is the sole responder in a thread, however that approach fails 8to properly account for various scenarios that occur with WSGI applications 9and middleware. 10 11What is actually needed in the case where a module global is desired that 12is always set properly depending on the current request, is a stacked 13thread-local object. Such an object is popped or pushed during the request 14cycle so that it properly represents the object that should be active for 15the current request. 16 17To make it easy to deal with such variables, this module provides a special 18StackedObjectProxy class which you can instantiate and attach to your 19module where you'd like others to access it. The object you'd like this to 20actually "be" during the request is then registered with the 21RegistryManager middleware, which ensures that for the scope of the current 22WSGI application everything will work properly. 23 24Example: 25 26.. code-block:: python 27 28 #yourpackage/__init__.py 29 30 from paste.registry import RegistryManager, StackedObjectProxy 31 myglobal = StackedObjectProxy() 32 33 #wsgi app stack 34 app = RegistryManager(yourapp) 35 36 #inside your wsgi app 37 class yourapp(object): 38 def __call__(self, environ, start_response): 39 obj = someobject # The request-local object you want to access 40 # via yourpackage.myglobal 41 if environ.has_key('paste.registry'): 42 environ['paste.registry'].register(myglobal, obj) 43 44You will then be able to import yourpackage anywhere in your WSGI app or in 45the calling stack below it and be assured that it is using the object you 46registered with Registry. 47 48RegistryManager can be in the WSGI stack multiple times, each time it 49appears it registers a new request context. 50 51 52Performance 53=========== 54 55The overhead of the proxy object is very minimal, however if you are using 56proxy objects extensively (Thousands of accesses per request or more), there 57are some ways to avoid them. A proxy object runs approximately 3-20x slower 58than direct access to the object, this is rarely your performance bottleneck 59when developing web applications. 60 61Should you be developing a system which may be accessing the proxy object 62thousands of times per request, the performance of the proxy will start to 63become more noticeable. In that circumstance, the problem can be avoided by 64getting at the actual object via the proxy with the ``_current_obj`` function: 65 66.. code-block:: python 67 68 #sessions.py 69 Session = StackedObjectProxy() 70 # ... initialization code, etc. 71 72 # somemodule.py 73 import sessions 74 75 def somefunc(): 76 session = sessions.Session._current_obj() 77 # ... tons of session access 78 79This way the proxy is used only once to retrieve the object for the current 80context and the overhead is minimized while still making it easy to access 81the underlying object. The ``_current_obj`` function is preceded by an 82underscore to more likely avoid clashing with the contained object's 83attributes. 84 85**NOTE:** This is *highly* unlikely to be an issue in the vast majority of 86cases, and requires incredibly large amounts of proxy object access before 87one should consider the proxy object to be causing slow-downs. This section 88is provided solely in the extremely rare case that it is an issue so that a 89quick way to work around it is documented. 90 91""" 92import six 93import paste.util.threadinglocal as threadinglocal 94 95__all__ = ['StackedObjectProxy', 'RegistryManager', 'StackedObjectRestorer', 96 'restorer'] 97 98class NoDefault(object): pass 99 100class StackedObjectProxy(object): 101 """Track an object instance internally using a stack 102 103 The StackedObjectProxy proxies access to an object internally using a 104 stacked thread-local. This makes it safe for complex WSGI environments 105 where access to the object may be desired in multiple places without 106 having to pass the actual object around. 107 108 New objects are added to the top of the stack with _push_object while 109 objects can be removed with _pop_object. 110 111 """ 112 def __init__(self, default=NoDefault, name="Default"): 113 """Create a new StackedObjectProxy 114 115 If a default is given, its used in every thread if no other object 116 has been pushed on. 117 118 """ 119 self.__dict__['____name__'] = name 120 self.__dict__['____local__'] = threadinglocal.local() 121 if default is not NoDefault: 122 self.__dict__['____default_object__'] = default 123 124 def __dir__(self): 125 """Return a list of the StackedObjectProxy's and proxied 126 object's (if one exists) names. 127 """ 128 dir_list = dir(self.__class__) + self.__dict__.keys() 129 try: 130 dir_list.extend(dir(self._current_obj())) 131 except TypeError: 132 pass 133 dir_list.sort() 134 return dir_list 135 136 def __getattr__(self, attr): 137 return getattr(self._current_obj(), attr) 138 139 def __setattr__(self, attr, value): 140 setattr(self._current_obj(), attr, value) 141 142 def __delattr__(self, name): 143 delattr(self._current_obj(), name) 144 145 def __getitem__(self, key): 146 return self._current_obj()[key] 147 148 def __setitem__(self, key, value): 149 self._current_obj()[key] = value 150 151 def __delitem__(self, key): 152 del self._current_obj()[key] 153 154 def __call__(self, *args, **kw): 155 return self._current_obj()(*args, **kw) 156 157 def __repr__(self): 158 try: 159 return repr(self._current_obj()) 160 except (TypeError, AttributeError): 161 return '<%s.%s object at 0x%x>' % (self.__class__.__module__, 162 self.__class__.__name__, 163 id(self)) 164 165 def __iter__(self): 166 return iter(self._current_obj()) 167 168 def __len__(self): 169 return len(self._current_obj()) 170 171 def __contains__(self, key): 172 return key in self._current_obj() 173 174 def __nonzero__(self): 175 return bool(self._current_obj()) 176 177 def _current_obj(self): 178 """Returns the current active object being proxied to 179 180 In the event that no object was pushed, the default object if 181 provided will be used. Otherwise, a TypeError will be raised. 182 183 """ 184 try: 185 objects = self.____local__.objects 186 except AttributeError: 187 objects = None 188 if objects: 189 return objects[-1] 190 else: 191 obj = self.__dict__.get('____default_object__', NoDefault) 192 if obj is not NoDefault: 193 return obj 194 else: 195 raise TypeError( 196 'No object (name: %s) has been registered for this ' 197 'thread' % self.____name__) 198 199 def _push_object(self, obj): 200 """Make ``obj`` the active object for this thread-local. 201 202 This should be used like: 203 204 .. code-block:: python 205 206 obj = yourobject() 207 module.glob = StackedObjectProxy() 208 module.glob._push_object(obj) 209 try: 210 ... do stuff ... 211 finally: 212 module.glob._pop_object(conf) 213 214 """ 215 try: 216 self.____local__.objects.append(obj) 217 except AttributeError: 218 self.____local__.objects = [] 219 self.____local__.objects.append(obj) 220 221 def _pop_object(self, obj=None): 222 """Remove a thread-local object. 223 224 If ``obj`` is given, it is checked against the popped object and an 225 error is emitted if they don't match. 226 227 """ 228 try: 229 popped = self.____local__.objects.pop() 230 if obj and popped is not obj: 231 raise AssertionError( 232 'The object popped (%s) is not the same as the object ' 233 'expected (%s)' % (popped, obj)) 234 except AttributeError: 235 raise AssertionError('No object has been registered for this thread') 236 237 def _object_stack(self): 238 """Returns all of the objects stacked in this container 239 240 (Might return [] if there are none) 241 """ 242 try: 243 try: 244 objs = self.____local__.objects 245 except AttributeError: 246 return [] 247 return objs[:] 248 except AssertionError: 249 return [] 250 251 # The following methods will be swapped for their original versions by 252 # StackedObjectRestorer when restoration is enabled. The original 253 # functions (e.g. _current_obj) will be available at _current_obj_orig 254 255 def _current_obj_restoration(self): 256 request_id = restorer.in_restoration() 257 if request_id: 258 return restorer.get_saved_proxied_obj(self, request_id) 259 return self._current_obj_orig() 260 _current_obj_restoration.__doc__ = \ 261 ('%s\n(StackedObjectRestorer restoration enabled)' % \ 262 _current_obj.__doc__) 263 264 def _push_object_restoration(self, obj): 265 if not restorer.in_restoration(): 266 self._push_object_orig(obj) 267 _push_object_restoration.__doc__ = \ 268 ('%s\n(StackedObjectRestorer restoration enabled)' % \ 269 _push_object.__doc__) 270 271 def _pop_object_restoration(self, obj=None): 272 if not restorer.in_restoration(): 273 self._pop_object_orig(obj) 274 _pop_object_restoration.__doc__ = \ 275 ('%s\n(StackedObjectRestorer restoration enabled)' % \ 276 _pop_object.__doc__) 277 278class Registry(object): 279 """Track objects and stacked object proxies for removal 280 281 The Registry object is instantiated a single time for the request no 282 matter how many times the RegistryManager is used in a WSGI stack. Each 283 RegistryManager must call ``prepare`` before continuing the call to 284 start a new context for object registering. 285 286 Each context is tracked with a dict inside a list. The last list 287 element is the currently executing context. Each context dict is keyed 288 by the id of the StackedObjectProxy instance being proxied, the value 289 is a tuple of the StackedObjectProxy instance and the object being 290 tracked. 291 292 """ 293 def __init__(self): 294 """Create a new Registry object 295 296 ``prepare`` must still be called before this Registry object can be 297 used to register objects. 298 299 """ 300 self.reglist = [] 301 302 def prepare(self): 303 """Used to create a new registry context 304 305 Anytime a new RegistryManager is called, ``prepare`` needs to be 306 called on the existing Registry object. This sets up a new context 307 for registering objects. 308 309 """ 310 self.reglist.append({}) 311 312 def register(self, stacked, obj): 313 """Register an object with a StackedObjectProxy""" 314 myreglist = self.reglist[-1] 315 stacked_id = id(stacked) 316 if stacked_id in myreglist: 317 stacked._pop_object(myreglist[stacked_id][1]) 318 del myreglist[stacked_id] 319 stacked._push_object(obj) 320 myreglist[stacked_id] = (stacked, obj) 321 322 def multiregister(self, stacklist): 323 """Register a list of tuples 324 325 Similar call semantics as register, except this registers 326 multiple objects at once. 327 328 Example:: 329 330 registry.multiregister([(sop, obj), (anothersop, anotherobj)]) 331 332 """ 333 myreglist = self.reglist[-1] 334 for stacked, obj in stacklist: 335 stacked_id = id(stacked) 336 if stacked_id in myreglist: 337 stacked._pop_object(myreglist[stacked_id][1]) 338 del myreglist[stacked_id] 339 stacked._push_object(obj) 340 myreglist[stacked_id] = (stacked, obj) 341 342 # Replace now does the same thing as register 343 replace = register 344 345 def cleanup(self): 346 """Remove all objects from all StackedObjectProxy instances that 347 were tracked at this Registry context""" 348 for stacked, obj in six.itervalues(self.reglist[-1]): 349 stacked._pop_object(obj) 350 self.reglist.pop() 351 352class RegistryManager(object): 353 """Creates and maintains a Registry context 354 355 RegistryManager creates a new registry context for the registration of 356 StackedObjectProxy instances. Multiple RegistryManager's can be in a 357 WSGI stack and will manage the context so that the StackedObjectProxies 358 always proxy to the proper object. 359 360 The object being registered can be any object sub-class, list, or dict. 361 362 Registering objects is done inside a WSGI application under the 363 RegistryManager instance, using the ``environ['paste.registry']`` 364 object which is a Registry instance. 365 366 """ 367 def __init__(self, application, streaming=False): 368 self.application = application 369 self.streaming = streaming 370 371 def __call__(self, environ, start_response): 372 app_iter = None 373 reg = environ.setdefault('paste.registry', Registry()) 374 reg.prepare() 375 if self.streaming: 376 return self.streaming_iter(reg, environ, start_response) 377 378 try: 379 app_iter = self.application(environ, start_response) 380 except Exception as e: 381 # Regardless of if the content is an iterable, generator, list 382 # or tuple, we clean-up right now. If its an iterable/generator 383 # care should be used to ensure the generator has its own ref 384 # to the actual object 385 if environ.get('paste.evalexception'): 386 # EvalException is present in the WSGI stack 387 expected = False 388 for expect in environ.get('paste.expected_exceptions', []): 389 if isinstance(e, expect): 390 expected = True 391 if not expected: 392 # An unexpected exception: save state for EvalException 393 restorer.save_registry_state(environ) 394 reg.cleanup() 395 raise 396 except: 397 # Save state for EvalException if it's present 398 if environ.get('paste.evalexception'): 399 restorer.save_registry_state(environ) 400 reg.cleanup() 401 raise 402 else: 403 reg.cleanup() 404 405 return app_iter 406 407 def streaming_iter(self, reg, environ, start_response): 408 try: 409 for item in self.application(environ, start_response): 410 yield item 411 except Exception as e: 412 # Regardless of if the content is an iterable, generator, list 413 # or tuple, we clean-up right now. If its an iterable/generator 414 # care should be used to ensure the generator has its own ref 415 # to the actual object 416 if environ.get('paste.evalexception'): 417 # EvalException is present in the WSGI stack 418 expected = False 419 for expect in environ.get('paste.expected_exceptions', []): 420 if isinstance(e, expect): 421 expected = True 422 if not expected: 423 # An unexpected exception: save state for EvalException 424 restorer.save_registry_state(environ) 425 reg.cleanup() 426 raise 427 except: 428 # Save state for EvalException if it's present 429 if environ.get('paste.evalexception'): 430 restorer.save_registry_state(environ) 431 reg.cleanup() 432 raise 433 else: 434 reg.cleanup() 435 436 437class StackedObjectRestorer(object): 438 """Track StackedObjectProxies and their proxied objects for automatic 439 restoration within EvalException's interactive debugger. 440 441 An instance of this class tracks all StackedObjectProxy state in existence 442 when unexpected exceptions are raised by WSGI applications housed by 443 EvalException and RegistryManager. Like EvalException, this information is 444 stored for the life of the process. 445 446 When an unexpected exception occurs and EvalException is present in the 447 WSGI stack, save_registry_state is intended to be called to store the 448 Registry state and enable automatic restoration on all currently registered 449 StackedObjectProxies. 450 451 With restoration enabled, those StackedObjectProxies' _current_obj 452 (overwritten by _current_obj_restoration) method's strategy is modified: 453 it will return its appropriate proxied object from the restorer when 454 a restoration context is active in the current thread. 455 456 The StackedObjectProxies' _push/pop_object methods strategies are also 457 changed: they no-op when a restoration context is active in the current 458 thread (because the pushing/popping work is all handled by the 459 Registry/restorer). 460 461 The request's Registry objects' reglists are restored from the restorer 462 when a restoration context begins, enabling the Registry methods to work 463 while their changes are tracked by the restorer. 464 465 The overhead of enabling restoration is negligible (another threadlocal 466 access for the changed StackedObjectProxy methods) for normal use outside 467 of a restoration context, but worth mentioning when combined with 468 StackedObjectProxies normal overhead. Once enabled it does not turn off, 469 however: 470 471 o Enabling restoration only occurs after an unexpected exception is 472 detected. The server is likely to be restarted shortly after the exception 473 is raised to fix the cause 474 475 o StackedObjectRestorer is only enabled when EvalException is enabled (not 476 on a production server) and RegistryManager exists in the middleware 477 stack""" 478 def __init__(self): 479 # Registries and their saved reglists by request_id 480 self.saved_registry_states = {} 481 self.restoration_context_id = threadinglocal.local() 482 483 def save_registry_state(self, environ): 484 """Save the state of this request's Registry (if it hasn't already been 485 saved) to the saved_registry_states dict, keyed by the request's unique 486 identifier""" 487 registry = environ.get('paste.registry') 488 if not registry or not len(registry.reglist) or \ 489 self.get_request_id(environ) in self.saved_registry_states: 490 # No Registry, no state to save, or this request's state has 491 # already been saved 492 return 493 494 self.saved_registry_states[self.get_request_id(environ)] = \ 495 (registry, registry.reglist[:]) 496 497 # Tweak the StackedObjectProxies we want to save state for -- change 498 # their methods to act differently when a restoration context is active 499 # in the current thread 500 for reglist in registry.reglist: 501 for stacked, obj in six.itervalues(reglist): 502 self.enable_restoration(stacked) 503 504 def get_saved_proxied_obj(self, stacked, request_id): 505 """Retrieve the saved object proxied by the specified 506 StackedObjectProxy for the request identified by request_id""" 507 # All state for the request identified by request_id 508 reglist = self.saved_registry_states[request_id][1] 509 510 # The top of the stack was current when the exception occurred 511 stack_level = len(reglist) - 1 512 stacked_id = id(stacked) 513 while True: 514 if stack_level < 0: 515 # Nothing registered: Call _current_obj_orig to raise a 516 # TypeError 517 return stacked._current_obj_orig() 518 context = reglist[stack_level] 519 if stacked_id in context: 520 break 521 # This StackedObjectProxy may not have been registered by the 522 # RegistryManager that was active when the exception was raised -- 523 # continue searching down the stack until it's found 524 stack_level -= 1 525 return context[stacked_id][1] 526 527 def enable_restoration(self, stacked): 528 """Replace the specified StackedObjectProxy's methods with their 529 respective restoration versions. 530 531 _current_obj_restoration forces recovery of the saved proxied object 532 when a restoration context is active in the current thread. 533 534 _push/pop_object_restoration avoid pushing/popping data 535 (pushing/popping is only done at the Registry level) when a restoration 536 context is active in the current thread""" 537 if '_current_obj_orig' in stacked.__dict__: 538 # Restoration already enabled 539 return 540 541 for func_name in ('_current_obj', '_push_object', '_pop_object'): 542 orig_func = getattr(stacked, func_name) 543 restoration_func = getattr(stacked, func_name + '_restoration') 544 stacked.__dict__[func_name + '_orig'] = orig_func 545 stacked.__dict__[func_name] = restoration_func 546 547 def get_request_id(self, environ): 548 """Return a unique identifier for the current request""" 549 from paste.evalexception.middleware import get_debug_count 550 return get_debug_count(environ) 551 552 def restoration_begin(self, request_id): 553 """Enable a restoration context in the current thread for the specified 554 request_id""" 555 if request_id in self.saved_registry_states: 556 # Restore the old Registry object's state 557 registry, reglist = self.saved_registry_states[request_id] 558 registry.reglist = reglist 559 560 self.restoration_context_id.request_id = request_id 561 562 def restoration_end(self): 563 """Register a restoration context as finished, if one exists""" 564 try: 565 del self.restoration_context_id.request_id 566 except AttributeError: 567 pass 568 569 def in_restoration(self): 570 """Determine if a restoration context is active for the current thread. 571 Returns the request_id it's active for if so, otherwise False""" 572 return getattr(self.restoration_context_id, 'request_id', False) 573 574restorer = StackedObjectRestorer() 575 576 577# Paste Deploy entry point 578def make_registry_manager(app, global_conf): 579 return RegistryManager(app) 580 581make_registry_manager.__doc__ = RegistryManager.__doc__ 582