Ticket #6199: cache_avoid_stampede.diff
File cache_avoid_stampede.diff, 9.2 KB (added by , 17 years ago) |
---|
-
django/conf/global_settings.py
283 283 CACHE_BACKEND = 'simple://' 284 284 CACHE_MIDDLEWARE_KEY_PREFIX = '' 285 285 CACHE_MIDDLEWARE_SECONDS = 600 286 CACHE_AVOID_STAMPEDE = False 286 287 287 288 #################### 288 289 # COMMENTS # -
django/core/cache/__init__.py
16 16 """ 17 17 18 18 from cgi import parse_qsl 19 from datetime import datetime, timedelta 20 19 21 from django.conf import settings 20 from django.core.cache.backends.base import InvalidCacheBackendError 22 from django.core.cache.backends.base import InvalidCacheBackendError, BaseCache 21 23 22 24 BACKENDS = { 23 25 # name for use in settings file --> name of module in "backends" directory … … 29 31 'dummy': 'dummy', 30 32 } 31 33 34 class WrappedValueSentinel(object): 35 pass 36 37 class NoStampedeWrapper(BaseCache): 38 """ 39 Avoids cache stampeding by missing on *one* request prior to actual 40 expiration. That single missed requestor then refills the cache prior to 41 real expiration. All other requestors still get hits. 42 43 Adapted from MintCache from gfranxman: 44 http://www.djangosnippets.org/snippets/155/ 45 """ 46 def __init__(self, backend, params): 47 self.backend = backend 48 49 near_miss_seconds = int(params.get('near_miss_seconds', 5)) 50 self.near_miss_delta = timedelta(seconds=near_miss_seconds) 51 52 super(NoStampedeWrapper, self).__init__(params) 53 54 def get(self, key, default=None): 55 inner_value = self.backend.get(key, None) 56 if inner_value is None: #real miss. 57 return default 58 59 #Ensure we packed this value. 60 #If not, return the value unaltered. 61 if not isinstance(inner_value, tuple): 62 return inner_value 63 64 try: 65 is_wrapped_sentinel, target_expire, value = inner_value 66 except ValueError: 67 return inner_value 68 69 if not isinstance(is_wrapped_sentinel, WrappedValueSentinel): 70 return inner_value 71 72 #We're now definitely dealing with managed value. 73 near_miss_time = target_expire - self.near_miss_delta 74 now = datetime.now() 75 76 if now >= near_miss_time: 77 #This is a near miss. 78 79 #Push out the target_expire out past real expiration 80 # to avoid extra misses. 81 #Use a new timeout that agrees with the originally 82 # requested expiration. 83 84 #Then return a miss to this requestor, who will then be 85 # responsible for filling the cache while any other 86 # requestors still get hits. 87 new_timeout = (target_expire - now).seconds 88 pushed_out = target_expire + timedelta(seconds=(new_timeout+1)) 89 90 new_inner_val = (is_wrapped_sentinel, 91 pushed_out, 92 value) 93 94 self.backend.set(key, new_inner_val, new_timeout) 95 96 return default 97 return value 98 99 def set(self, key, value, timeout=0): 100 timeout = timeout or self.default_timeout 101 target_expire = datetime.now() + timedelta(seconds=timeout) 102 103 inner_value = (WrappedValueSentinel(), 104 target_expire, 105 value) 106 107 self.backend.set(key, inner_value, timeout) 108 109 def delete(self, key): 110 self.backend.delete(key) 111 32 112 def get_cache(backend_uri): 33 113 if backend_uri.find(':') == -1: 34 114 raise InvalidCacheBackendError, "Backend URI must start with scheme://" … … 49 129 host = host[:-1] 50 130 51 131 cache_class = getattr(__import__('django.core.cache.backends.%s' % BACKENDS[scheme], {}, {}, ['']), 'CacheClass') 52 returncache_class(host, params)132 cache_backend = cache_class(host, params) 53 133 134 if settings.CACHE_AVOID_STAMPEDE: 135 cache_backend = NoStampedeWrapper(cache_backend, params) 136 return cache_backend 137 54 138 cache = get_cache(settings.CACHE_BACKEND) -
tests/regressiontests/cache/tests.py
2 2 3 3 # Unit tests for cache framework 4 4 # Uses whatever cache backend is set in the test settings file. 5 6 from django.core.cache import cache7 5 import time, unittest 8 6 7 from django.conf import settings 8 from django.core.cache import cache, get_cache, NoStampedeWrapper 9 from django.core.cache.backends.base import BaseCache 10 9 11 # functions/classes for complex data type tests 10 12 def f(): 11 13 return 42 … … 81 83 cache.set(key, value) 82 84 self.assertEqual(cache.get(key), value) 83 85 86 class StampedeCache(Cache): 87 def setUp(self): 88 self.prior_stampede = settings.CACHE_AVOID_STAMPEDE 89 settings.CACHE_AVOID_STAMPEDE = True 90 91 def tearDown(self): 92 settings.CACHE_AVOID_STAMPEDE = self.prior_stampede 93 94 def test_stampede_delta(self): 95 from datetime import timedelta 96 stuff = [ 97 ('simple://', timedelta(seconds=5)), 98 ('simple://?near_miss_seconds=30', timedelta(seconds=30)) 99 ] 100 for backend_uri, delta in stuff: 101 c = get_cache(backend_uri) 102 self.assertEqual(c.near_miss_delta, 103 delta) 104 def test_got_stampede_wrapper(self): 105 c = get_cache('simple://') 106 self.assertEqual(type(c), NoStampedeWrapper) 107 self.assert_(isinstance(c, BaseCache)) 108 109 def test_near_miss(self): 110 c = get_cache('simple://?near_miss_seconds=2') 111 c.set('test_near_miss', 'actual hit', 3) 112 time.sleep(1.1) 113 self.assertEqual(c.get('test_near_miss'), None) 114 self.assertEqual(c.get('test_near_miss'), 'actual hit') 115 116 def test_unwrapped_set(self): 117 c = get_cache('simple://?near_miss_seconds=2') 118 119 test_values = [ 120 1, 121 'unwrapped iterator', 122 ('unwrapped', 'three', 'tuple'), 123 ('unwrapped', 'shorter tuple'), 124 ] 125 126 for test_value in test_values: 127 c.backend.set('test_unwrapped_set', test_value, 5) 128 self.assertEqual(c.get('test_unwrapped_set'), test_value) 129 c.backend.set('test_unwrapped_set', test_value, 5) 130 84 131 if __name__ == '__main__': 85 132 unittest.main() -
docs/settings.txt
248 248 The default number of seconds to cache a page when the caching middleware or 249 249 ``cache_page()`` decorator is used. 250 250 251 CACHE_AVOID_STAMPEDE 252 -------------------- 253 254 Default: ``False`` 255 256 If True, the selected ``CACHE_BACKEND`` is wrapped to avoid stampeding on 257 cache misses. See the `cache docs`_. 258 251 259 DATABASE_ENGINE 252 260 --------------- 253 261 -
docs/cache.txt
203 203 dumped when max_entries is reached. This makes culling *much* faster 204 204 at the expense of more cache misses. 205 205 206 near_miss_seconds 207 The estimated number of seconds needed to reproduce a cache value. 208 Only used when ``CACHE_AVOID_STAMPEDE`` is True. Defaults to 5. 209 206 210 In this example, ``timeout`` is set to ``60``:: 207 211 208 212 CACHE_BACKEND = "memcached://127.0.0.1:11211/?timeout=60" … … 342 346 That's it. The cache has very few restrictions: You can cache any object that 343 347 can be pickled safely, although keys must be strings. 344 348 349 Avoiding cache stampedes 350 ======================== 351 352 Caching is intended to make your site more efficient by avoiding reproducing 353 expensive results. Django's cache does this by storing the expensive result 354 for a specific amount of time. This generally works very well. However, when 355 many requests for the same cache key occur in the time that it takes to produce 356 the cached result, the goal may not be achieved. For example, if 20 requests 357 per second are made to a key which takes 5 seconds to produce, the work to 358 produce the value corresponding to the key may be done as much as 100 (5 x 20) 359 times. 360 361 This condition is called stampeding (or dog-piling), and can be avoided by 362 setting ``CACHE_AVOID_STAMPEDE`` to ``True``. Doing so will slightly slow 363 down both ``get`` and ``set`` operations. It will also the value stored in 364 the cache backend by including values needed to manage the stampede condition. 365 366 Recommended for high traffic sites with uniform access to keys. 367 368 Note that ``CACHE_AVOID_STAMPEDE`` will not affect normal key expiration. 369 It will not avoid stampedes which occur just before the normal expiration of 370 the key because ``CACHE_AVOID_STAMPEDE`` will not extend the normal expiration 371 to avoid it. 372 345 373 Upstream caches 346 374 =============== 347 375